- see if we can use rsx on
componentscrate 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;fnmain(){// Compile our SQLcornucopia();}fncornucopia(){// 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
dbdependencies
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;pubuse cornucopia_async::Params;pubuse deadpool_postgres::{Pool,PoolError,Transaction};pubuse tokio_postgres::ErrorasTokioPostgresError;pubuse queries::users::User;pubfncreate_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 {usesuper::*;#[tokio::test]asyncfnload_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)]pubstructConfig{pubdatabase_url:String,}implConfig{pubfnnew() -> 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)]pubenumCustomError{FaultySetup(String),Database(String),}// Allow the use of "{}" format specifierimpl fmt::DisplayforCustomError{fnfmt(&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?implIntoResponseforCustomError{fninto_response(self) -> Response{let(status, error_message) = matchself{CustomError::Database(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),CustomError::FaultySetup(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),};format!("status = {}, message = {}", status, error_message).into_response()}}implFrom<axum::http::uri::InvalidUri>forCustomError{fnfrom(err: axum::http::uri::InvalidUri) -> CustomError{CustomError::FaultySetup(err.to_string())}}implFrom<TokioPostgresError>forCustomError{fnfrom(err:TokioPostgresError) -> CustomError{CustomError::Database(err.to_string())}}implFrom<PoolError>forCustomError{fnfrom(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;usecrate::errors::CustomError;use axum::{extract::Extension, response::Json, routing::get,Router};use db::User;use std::net::SocketAddr;#[tokio::main]asyncfnmain(){let config = config::Config::new();let pool = db::create_pool(&config.database_url);// build our application with a routelet app = Router::new().route("/",get(users)).layer(Extension(config)).layer(Extension(pool.clone()));// run itlet 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();}asyncfnusers(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]pubfnLayout(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
usecrate::layout::Layout;use db::User;use dioxus::prelude::*;// Define the properties for IndexPage#[derive(Props,Clone,PartialEq)]// Add Clone and PartialEq herepubstructIndexPageProps{pubusers:Vec<User>,}// Define the IndexPage component#[component]pubfnIndexPage(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;pubmod users;use dioxus::prelude::*;pubfnrender(mutvirtual_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
servercrateupdate dependencies
cargo add dioxus
cargo add --path ../pages- update
main.rs
main.rs
mod config;mod errors;usecrate::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]asyncfnmain(){let config = config::Config::new();let pool = db::create_pool(&config.database_url);// build our application with a routelet app = Router::new().route("/",get(users)).layer(Extension(config)).layer(Extension(pool.clone()));// run itlet 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();}pubasyncfnusers(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.svgfile onimagesfolder
avatar.svg
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 512 512">
<pathfill="#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.rsupdate
build.rs
build.rs
use ructe::{Result,Ructe};fnmain() -> Result<()>{letmut ructe = Ructe::from_env().unwrap();letmut 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"));pubuse templates::statics as files;cd to
servercratecreate
static_files.rsupdate
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;pubasyncfnstatic_path(Path(path):Path<String>) -> implIntoResponse{let path = path.trim_start_matches('/');ifletSome(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.rsto add the new route for static files
main.rs
// load modulemod 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 avataruse assets::files::avatar_svg;
...// access the static fileimg{
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
cdylibandrlibadd 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]pubfnsay_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)]pubfnstart() -> Result<(),JsValue>{// Access the DOM window objectlet window = window().unwrap();let document = window.document().unwrap();// Get the button element by IDlet button:Element = document.get_element_by_id("alert-btn").unwrap();// Set an event listener for the button clicklet closure = Closure::wrap(Box::new(move || {// Call the Rust function say_hellosay_hello();})asBox<dynFn()>);// 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/usersfolder - go back to
componentscrate - 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]fnsome_function(){// Implementation for feature1}