Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wasm): doesn't work #12

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,32 @@ name = "stoic-quotes"
version = "0.3.5"
edition = "2021"
authors = ["Jose Storopoli <jose@storopoli.io>"]
description = "Stoic quotes API backend"
description = "Stoic Quotes PWA"
license = "MIT"
readme = "README.md"

[dependencies]
anyhow = "1"
askama = "0.12"
axum = "0.7"
lazy_static = "1"
prest = "0.2"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["tracing", "trace", "fs"] }
tracing = "0.1"
tracing-subscriber = "0.3"
wasm-bindgen = "0.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
http-body-util = "0.1"
tokio = { version = "1", features = ["rt", "macros"] }
tower = { version = "0.4", features = ["util"] }

[build-dependencies]
prest-build = "0.2"

[profile.release]
lto = true
opt-level = "z" # Optimized for size, use 3 for speed
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduced to increase optimizations
panic = "abort" # Abort on panic
strip = "symbols" # Strip symbols from binary
5 changes: 5 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use prest_build::*;

fn main() {
build_pwa(PWAOptions::default()).unwrap();
}
17 changes: 4 additions & 13 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::clone::Clone;

lazy_static! {
/// A vector of [`Quote`]s
static ref QUOTES: Vec<Quote> = read_data().expect("failed to read data");
pub static ref QUOTES: Vec<Quote> = read_data().expect("failed to read data");
}

/// A quote with text and author
Expand All @@ -18,16 +18,7 @@ lazy_static! {
///
/// * `text` - A [`String`] that contains the quote text
/// * `author` - A [`String`] that contains the quote author
///
/// # Examples
///
/// ```
/// let quote = Quote {
/// text: "Hello, world!".to_string(),
/// author: "Anonymous".to_string(),
/// };
/// ```
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Quote {
pub text: String,
pub author: String,
Expand All @@ -45,8 +36,8 @@ pub fn read_data() -> Result<Vec<Quote>> {
/// Currently, this function uses [`rand::thread_rng()`] to generate
/// a random number between 0 and the length of [`struct@QUOTES`].
/// Then, it clones the [`struct@QUOTES`] at the random index and returns it.
pub fn random_quote() -> Quote {
pub fn random_quote() -> &'static Quote {
let mut rng = thread_rng();
let index = rng.gen_range(0..QUOTES.len());
QUOTES[index].clone()
&QUOTES[index]
}
57 changes: 37 additions & 20 deletions src/app.rs → src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
//! Module that has functions that handles the Axum [`Router`].

use crate::pages::{plain_quote, quote, root};
use axum::{http::header::USER_AGENT, http::Request, response::Response, routing::get, Router};
use std::{env::current_dir, path::PathBuf};
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::info;
//! # Stoic Quotes
//!
//! `stoic-quotes` is a collection of stoic quotes in an Axum web server
//! that serves stoic quotes with reactivity by the all-mighty
//! [htmx](https://htmx.org) (no YAVASCRIPT).
//!
//! It also has plain-text API GET endpoints at `/` that returns a stoic quote
//! for terminal users with `curl` and `wget`.

pub mod data;
pub mod pages;

use crate::pages::{home, plain_quote, quote};
use prest::header::USER_AGENT;
use prest::*;

/// Handles the User Agent header
/// If the user agent is `curl` or `wget`,
/// return a plain quote.
/// Otherwise, return the root page.
async fn handle_user_agent<T>(req: Request<T>) -> Response {
async fn handle_user_agent<T>(req: Request<T>) -> Markup {
let header = Request::headers(&req);
let user_agent: String = if let Some(user_agent) = header.get(USER_AGENT) {
user_agent.clone().to_str().unwrap().to_string()
Expand All @@ -21,37 +29,46 @@ async fn handle_user_agent<T>(req: Request<T>) -> Response {
info!("got user agent: {user_agent}");

if user_agent.contains("curl") || user_agent.contains("Wget") {
plain_quote().await
html! {(plain_quote().await)}
} else {
root().await
home().await
}
}

pub async fn into_page(content: Markup) -> Markup {
let title = "Stoic Quotes";
let css = "/assets/main.css";
html! {(DOCTYPE) {
(Head::default().title(title).css(css))
(content)
(Scripts::default())
}}
}

/// Creates an Axum [`Router`] that only handles GET requests to
/// `/` and `/quote`.
pub fn app() -> Router {
let assets_path: PathBuf = current_dir().unwrap();
// Create a router
info!("initializing router...");
Router::new()
.route("/", get(handle_user_agent))
.route("/quote", get(quote))
.nest_service(
"/assets",
ServeDir::new(format!("{}/assets", assets_path.to_str().unwrap())),
)
// We can still add middleware
.layer(TraceLayer::new_for_http())
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(start)]
pub fn main() {
app().handle_fetch_events()
}

#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
use http_body_util::BodyExt; // for `collect`
use prest::{
http::{Request, StatusCode},
Body,
};
use http_body_util::BodyExt; // for `collect`
use std::str::from_utf8;
use tower::{Service, ServiceExt}; // for `call`, `oneshot`, and `ready`

Expand Down
43 changes: 5 additions & 38 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,9 @@
//! # Stoic Quotes
//!
//! `stoic-quotes` is a collection of stoic quotes in an Axum web server
//! that serves stoic quotes with reactivity by the all-mighty
//! [htmx](https://htmx.org) (no YAVASCRIPT).
//!
//! It also has plain-text API GET endpoints at `/` that returns a stoic quote
//! for terminal users with `curl` and `wget`.
use prest::*;

use anyhow::{Context, Result};
use axum::serve;
use tokio::net::TcpListener;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
use stoic_quotes::{app, into_page};

mod app;
mod data;
mod pages;
embed_build_output_as!(BuiltAssets);

use app::app;

#[tokio::main]
async fn main() -> Result<()> {
// Set up logging
let subscriber = FmtSubscriber::builder()
// all spans/events with a level higher than TRACE (e.g, debug, info, warn, etc.)
// will be written to stdout.
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

let app = app();

// run our app with hyper, listening globally on port 3000
let port = 3000_u16;
let addr = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
info!("router initialized, listening on port {:?}", port);
serve(addr, app)
.await
.context("error while starting server")?;
Ok(())
fn main() {
app().wrap_non_htmx(into_page).embed(BuiltAssets).run();
}
100 changes: 46 additions & 54 deletions src/pages.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,61 @@
//! Module that has functions and structs that handles the Askama templates
//! to be rendered as HTML responses by Axum.

use crate::data::{random_quote, Quote};
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
use crate::{
data::{random_quote, Quote},
into_page,
};

/// A wrapper type that we'll use to encapsulate HTML parsed by askama
/// into valid HTML for axum to serve.
pub struct HtmlTemplate<T>(T);

/// Allows us to convert Askama HTML templates into valid HTML for axum
/// to serve in the response.
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
// Attempt to render the template with askama
match self.0.render() {
// If we're able to successfully parse and aggregate the template, serve it
Ok(html) => Html(html).into_response(),
// If we're not, return an error or some bit of fallback HTML
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {}", err),
)
.into_response(),
use prest::{html, Markup};

pub async fn quote() -> Markup {
let quote = random_quote();
let text = &quote.text;
let author = &quote.author;

html! {
blockquote #quote .text-center {
p ."text-2xl font-semibold md:text-4xl" {
(text)
}
footer ."mt-4 text-xl text-right italic" {
(author)
}
}
}
}

/// An askama template that we'll use to render the root HTML element.
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {}

/// An askama template that we'll use to render the quote HTML elements.
/// It has htmx attributes that allow us to refresh the quote without
/// refreshing the entire page.
#[derive(Template)]
#[template(path = "quote.html")]
struct QuoteTemplate {
text: String,
author: String,
}

/// Returns the rendered askama template for the root HTML element.
pub async fn root() -> Response {
let template = IndexTemplate {};
HtmlTemplate(template).into_response()
}

/// Returns the rendered askama template for a random quote HTML element.
pub async fn quote() -> Response {
let Quote { text, author } = random_quote();
let template = QuoteTemplate { text, author };
HtmlTemplate(template).into_response()
pub async fn home() -> Markup {
into_page(html!(
div."min-h-screen flex flex-col items-center justify-center text-white" {
div."card card-bordered border-accent bg-base-300 shadow-2xl mx-4 p-10 rounded-lg" {
blockquote hx-get="/quote" hx-swap="outerHTML" hx-trigger="load" #quote ."text-center" {
"Please Enable JavaScript (I know, it sucks...)"
}
}
button."btn btn-accent bg-base-300 mt-10" hx-get="/quote" hx-trigger="click" hx-target="#quote" hx-swap="outerHTML transition:true"
aria-label="Refresh" {
svg."w-6 h-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" {
path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" {}
}
}
a."mt-10 flex hover:text-accent" aria-label="Go to the GitHub repository with the code"
href="https://github.com/storopoli/stoic-quotes" target="_blank" rel="noopener noreferrer" {
svg ."w-8 h-8" fill="currentColor" viewBox="0 0 24 24" {
path fill="evenodd" clip="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.866-.014-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.532 1.03 1.532 1.03.891 1.529 2.341 1.088 2.912.833.091-.646.349-1.086.635-1.337-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.03-2.682-.103-.253-.447-1.27.098-2.646 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 7.07c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.026 2.747-1.026.547 1.376.203 2.394.1 2.646.64.699 1.028 1.591 1.028 2.682 0 3.841-2.337 4.687-4.565 4.934.359.31.678.92.678 1.852 0 1.336-.012 2.415-.012 2.741 0 .267.18.578.688.48A10.017 10.017 0 0022 12C22 6.477 17.523 2 12 2z" {}
}
"storopoli/stoic-quotes"
}
}
)).await
}

/// Returns a plain text random quote without any HTML.
pub async fn plain_quote() -> Response {
pub async fn plain_quote() -> String {
let Quote { text, author } = random_quote();
let formatted_quote: String = format!("{text}\n\n - {author}\n");
Html(formatted_quote).into_response()
formatted_quote
}
Loading