Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dioxus Fullstack (Island Architecture)

TODO :

  • see if we can use rsx on components crate so we dont have to write web-sys to access DOM elements
Screenshot Screenshot 2024-10-21 at 6 48 22 AM

Requirements

  • geni cli
  • dx cli
  • wasm-bindgen-cli
  • wasm-pack
  • cargo-runner

Initialize the app

  1. mkdir hexagonal
  2. ws init
  3. add new members on Cargo.toml
[workspace]
resolver = "2"
members = ["server", "components", "pages"]
  1. copy .gitignore
  2. generate crates
cargo new server
cargo new components --lib
cargo new pages --lib
  1. init

Set migrations

  1. export DATABASE_URL=postgres://postgres:postgres@localhost:5432/hexagonal

  2. geni create

  3. geni new create_users_table

  4. geni up

Add DB Queries

  1. cargo init --lib db
  2. cd db
  3. mkdir queries
  4. add initial queries

e.g. users.sql

--: User()

--! get_users : User
SELECT 
    id, 
    email
FROM users;
  1. 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());
    }
}
  1. Add db dependencies
cargo add [email protected]
cargo add [email protected]
cargo add [email protected]
cargo add [email protected]
cargo add tokio@1 --features macros,rt-multi-thread
cargo add [email protected]
cargo add serde@1 --features derive
  1. 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);
    }
}
  1. build the crate and run the tests using cargo-runner

Set up server crate

  1. cd server
  2. 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 }
    }
}
  1. 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())
    }
}
  1. Add dependencies
cargo add [email protected] --no-default-features -F json,http1,tokio
cargo add tokio@1 --no-default-features -F macros,fs,rt-multi-thread
cargo add --path ../db
  1. 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))
}
  1. run the server

Set up Pages crate

  1. cargo init --lib pages
  2. cd pages
  3. install dependencies
cargo add dioxus 
cargo adddioxus-ssr
cargo add --path ../db
  1. 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} }
    )
}
  1. 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}" }
                        }
                    }
                }
            }
        }
    }
}
  1. 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)
}
  1. cd to server crate

  2. update dependencies

cargo add dioxus
cargo add --path ../pages
  1. 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))
}
  1. run the server

Set up assets crate for static files

  1. cargo init --lib assets
  2. cd assets
  3. mkdir images
  4. create an avatar.svg file on images 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>
  1. touch build.rs

  2. 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(())
}
  1. add dependencies
cargo add [email protected]
cargo add --build [email protected] --no-default-features -F mime03
  1. update lib.rs
lib.rs
include!(concat!(env!("OUT_DIR"), "/templates.rs"));

pub use templates::statics as files;
  1. cd to server crate

  2. create static_files.rs

  3. 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()
    }
}
  1. 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()));
... 
  1. cargo add --path ../assets

  2. 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"
}
  1. run the server

Set up Components crate

  1. cargo init --lib components

  2. 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"] }
  1. set the crate type to cdylib and rlib

  2. 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(())
}
  1. cd to assets crate
  2. create js/pages/users folder
  3. go back to components crate
  4. generate assets using command
wasm-pack build --target web --out-dir ../assets/js/pages/users
  1. Use the generated asset on pages/src/users.rs
            script {
                r#type: "module",
                dangerous_inner_html: r#"
import init from '/static/components.js';
init();
"#
            }

Feature gating for wasm components

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
}

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages