- see if we can use rsx on
components
crate so we dont have to write web-sys to access DOM elements
- geni cli
- dx cli
- wasm-bindgen-cli
- wasm-pack
- cargo-runner
- mkdir hexagonal
- ws init
- add new members on
Cargo.toml
[workspace]
resolver = "2"
members = ["server", "components", "pages"]
- copy
.gitignore
- generate crates
cargo new server
cargo new components --lib
cargo new pages --lib
- init
-
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/hexagonal
-
geni create
-
geni new create_users_table
-
geni up
- cargo init --lib db
- cd db
- mkdir queries
- add initial queries
e.g. users.sql
--: User()
--! get_users : User
SELECT
id,
email
FROM users;
- touch build.rs
build.rs
use std::env;
use std::path::Path;
fn main() {
// Compile our SQL
cornucopia();
}
fn cornucopia() {
// For the sake of simplicity, this example uses the defaults.
let queries_path = "queries";
let out_dir = env::var_os("OUT_DIR").unwrap();
let file_path = Path::new(&out_dir).join("cornucopia.rs");
let db_url = env::var_os("DATABASE_URL").unwrap();
// Rerun this build script if the queries or migrations change.
println!("cargo:rerun-if-changed={queries_path}");
// Call cornucopia. Use whatever CLI command you need.
let output = std::process::Command::new("cornucopia")
.arg("-q")
.arg(queries_path)
.arg("--serialize")
.arg("-d")
.arg(&file_path)
.arg("live")
.arg(db_url)
.output()
.unwrap();
// If Cornucopia couldn't run properly, try to display the error.
if !output.status.success() {
panic!("{}", &std::str::from_utf8(&output.stderr).unwrap());
}
}
- Add
db
dependencies
cargo add cornucopia_async@0.6
cargo add tokio-postgres@0.7
cargo add deadpool-postgres@0.12
cargo add postgres-types@0.2
cargo add tokio@1 --features macros,rt-multi-thread
cargo add futures@0.3
cargo add serde@1 --features derive
- modify
lib.rs
lib.rs
use std::str::FromStr;
pub use cornucopia_async::Params;
pub use deadpool_postgres::{Pool, PoolError, Transaction};
pub use tokio_postgres::Error as TokioPostgresError;
pub use queries::users::User;
pub fn create_pool(database_url: &str) -> deadpool_postgres::Pool {
let config = tokio_postgres::Config::from_str(database_url).unwrap();
let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
deadpool_postgres::Pool::builder(manager).build().unwrap()
}
include!(concat!(env!("OUT_DIR"), "/cornucopia.rs"));
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn load_users() {
let db_url = std::env::var("DATABASE_URL").unwrap();
let pool = create_pool(&db_url);
let client = pool.get().await.unwrap();
//let transaction = client.transaction().await.unwrap();
let users = crate::queries::users::get_users()
.bind(&client)
.all()
.await
.unwrap();
dbg!(users);
}
}
- build the crate and run the tests using cargo-runner
- cd server
- touch src/config.rs
config.rs
#[derive(Clone, Debug)]
pub struct Config {
pub database_url: String,
}
impl Config {
pub fn new() -> Config {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
Config { database_url }
}
}
- touch src/errors.rs
errors.rs
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use db::{PoolError, TokioPostgresError};
use std::fmt;
#[derive(Debug)]
pub enum CustomError {
FaultySetup(String),
Database(String),
}
// Allow the use of "{}" format specifier
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CustomError::FaultySetup(ref cause) => write!(f, "Setup Error: {}", cause),
//CustomError::Unauthorized(ref cause) => write!(f, "Setup Error: {}", cause),
CustomError::Database(ref cause) => {
write!(f, "Database Error: {}", cause)
}
}
}
}
// So that errors get printed to the browser?
impl IntoResponse for CustomError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
CustomError::Database(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
CustomError::FaultySetup(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
};
format!("status = {}, message = {}", status, error_message).into_response()
}
}
impl From<axum::http::uri::InvalidUri> for CustomError {
fn from(err: axum::http::uri::InvalidUri) -> CustomError {
CustomError::FaultySetup(err.to_string())
}
}
impl From<TokioPostgresError> for CustomError {
fn from(err: TokioPostgresError) -> CustomError {
CustomError::Database(err.to_string())
}
}
impl From<PoolError> for CustomError {
fn from(err: PoolError) -> CustomError {
CustomError::Database(err.to_string())
}
}
- Add dependencies
cargo add axum@0.7 --no-default-features -F json,http1,tokio
cargo add tokio@1 --no-default-features -F macros,fs,rt-multi-thread
cargo add --path ../db
- update
main.rs
main.rs
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{extract::Extension, response::Json, routing::get, Router};
use db::User;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
// build our application with a route
let app = Router::new()
.route("/", get(users))
.layer(Extension(config))
.layer(Extension(pool.clone()));
// run it
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
async fn users(Extension(pool): Extension<db::Pool>) -> Result<Json<Vec<User>>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
Ok(Json(users))
}
- run the server
- cargo init --lib pages
- cd pages
- install dependencies
cargo add dioxusÂ
cargo adddioxus-ssr
cargo add --path ../db
- create
src/layout.rs
layout.rs
#![allow(non_snake_case)]
use dioxus::prelude::*;
#[component]
pub fn Layout(title: String, children: Element) -> Element {
rsx!(
head {
title { "{title}" }
meta { charset: "utf-8" }
meta { "http-equiv": "X-UA-Compatible", content: "IE=edge" }
meta {
name: "viewport",
content: "width=device-width, initial-scale=1"
}
}
body { {children} }
)
}
- create
src/users.rs
users.rs
use crate::layout::Layout;
use db::User;
use dioxus::prelude::*;
// Define the properties for IndexPage
#[derive(Props, Clone, PartialEq)] // Add Clone and PartialEq here
pub struct IndexPageProps {
pub users: Vec<User>,
}
// Define the IndexPage component
#[component]
pub fn IndexPage(props: IndexPageProps) -> Element {
rsx! {
Layout { title: "Users Table",
table {
thead {
tr {
th { "ID" }
th { "Email" }
}
}
tbody {
for user in props.users {
tr {
td {
strong { "{user.id}" }
}
td { "{user.email}" }
}
}
}
}
}
}
}
- update
src/lib.rs
lib.rs
mod layout;
pub mod users;
use dioxus::prelude::*;
pub fn render(mut virtual_dom: VirtualDom) -> String {
virtual_dom.rebuild_in_place();
let html = dioxus_ssr::render(&virtual_dom);
format!("<!DOCTYPE html><html lang='en'>{}</html>", html)
}
-
cd to
server
crate -
update dependencies
cargo add dioxus
cargo add --path ../pages
- update
main.rs
main.rs
mod config;
mod errors;
use crate::errors::CustomError;
use axum::response::Html;
use axum::{extract::Extension, routing::get, Router};
use dioxus::dioxus_core::VirtualDom;
use pages::{
render,
users::{IndexPage, IndexPageProps},
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
// build our application with a route
let app = Router::new()
.route("/", get(users))
.layer(Extension(config))
.layer(Extension(pool.clone()));
// run it
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on... {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
pub async fn users(Extension(pool): Extension<db::Pool>) -> Result<Html<String>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
let html = render(VirtualDom::new_with_props(
IndexPage,
IndexPageProps { users },
));
Ok(Html(html))
}
- run the server
- cargo init --lib assets
- cd assets
- mkdir images
- create an
avatar.svg
file onimages
folder
avatar.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="#fff" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM160 256c0-53 43-96 96-96h64c53 0 96-43 96-96s-43-96-96-96H160zm0 96c0 53 43 96 96 96h64c53 0 96-43 96-96s-43-96-96-96H160zm0 96c0 53 43 96 96 96h64c53 0 96-43 96-96s-43-96-96-96H160z"/>
</svg>
-
touch
build.rs
-
update
build.rs
build.rs
use ructe::{Result, Ructe};
fn main() -> Result<()> {
let mut ructe = Ructe::from_env().unwrap();
let mut statics = ructe.statics().unwrap();
statics.add_files("images").unwrap();
ructe.compile_templates("images").unwrap();
Ok(())
}
- add dependencies
cargo add mime@0.3
cargo add --build ructe@0.17 --no-default-features -F mime03
- update
lib.rs
lib.rs
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
pub use templates::statics as files;
-
cd to
server
crate -
create
static_files.rs
-
update
static_files.rs
static_files.rs
use assets::templates::statics::StaticFile;
use axum::body::Body;
use axum::extract::Path;
use axum::http::{header, HeaderValue, Response, StatusCode};
use axum::response::IntoResponse;
pub async fn static_path(Path(path): Path<String>) -> impl IntoResponse {
let path = path.trim_start_matches('/');
if let Some(data) = StaticFile::get(path) {
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_str(data.mime.as_ref()).unwrap(),
)
.body(Body::from(data.content))
.unwrap()
} else {
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.unwrap()
}
}
- modify
main.rs
to add the new route for static files
main.rs
// load module
mod static_files;
let app = Router::new()
.route("/", get(users))
.route("/static/*path", get(static_files::static_path)) // add this line
.layer(Extension(config))
.layer(Extension(pool.clone()));
...
-
cargo add --path ../assets
-
use the static files on
pages/src/users.rs
users.rs
// use avatar
use assets::files::avatar_svg;
...
// access the static file
img {
src: format!("/static/{}", avatar_svg.name),
width: "16",
height: "16"
}
- run the server
-
cargo init --lib components
-
add dependencies
dioxus = "0.5.6"
js-sys = "0.3.72"
wasm-bindgen = "0.2.93"
web-sys = { version = "0.3.72", features = ["Document", "Element", "HtmlElement", "Window", "console"] }
-
set the crate type to
cdylib
andrlib
-
add example code to test on
src/lib.rs
lib.rs
use js_sys::Math;
use wasm_bindgen::prelude::*;
use web_sys::{console, window, Element};
#[wasm_bindgen]
pub fn say_hello() {
let random_number = Math::random();
let message = format!("Hello from Rust! Random number: {}", random_number);
// Log to the browser console
console::log_1(&"Logging to console from Rust!".into());
console::log_1(&format!("Generated random number: {}", random_number).into());
// Show alert
web_sys::window()
.unwrap()
.alert_with_message(&message)
.unwrap();
}
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
// Access the DOM window object
let window = window().unwrap();
let document = window.document().unwrap();
// Get the button element by ID
let button: Element = document.get_element_by_id("alert-btn").unwrap();
// Set an event listener for the button click
let closure = Closure::wrap(Box::new(move || {
// Call the Rust function say_hello
say_hello();
}) as Box<dyn Fn()>);
// Set an event listener for the button click
button
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.set_onclick(Some(closure.as_ref().unchecked_ref()));
// We need to keep the closure alive, so we store it in memory.
closure.forget();
Ok(())
}
- cd to assets crate
- create
js/pages/users
folder - go back to
components
crate - generate assets using command
wasm-pack build --target web --out-dir ../assets/js/pages/users
- Use the generated asset on
pages/src/users.rs
script {
r#type: "module",
dangerous_inner_html: r#"
import init from '/static/components.js';
init();
"#
}
we need to use feature gating to only include components that are needed
wasm-pack build --target web --out-dir ../assets/js/pages/${feature} --features ${feature}
e.g.
[features]
default = []
users = []
featurex = []
on rust code we can do
#[cfg(feature = "feature1")]
#[wasm_bindgen]
fn some_function() {
// Implementation for feature1
}