diff --git a/.env b/.env new file mode 100644 index 0000000..ac78248 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=db/dev.db diff --git a/.gitignore b/.gitignore index ea8c4bf..0414a22 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +thirdparty +db/*.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7bbbb73..0a3a0ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,13 @@ name = "actix-example" version = "0.1.0" dependencies = [ "actix-web", + "diesel", + "dotenv", + "env_logger", + "log", + "serde", + "serde_json", + "uuid", "yew", ] @@ -236,6 +243,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -305,6 +323,12 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" @@ -403,6 +427,29 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" +dependencies = [ + "byteorder", + "diesel_derives", + "libsqlite3-sys", + "r2d2", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.3" @@ -413,6 +460,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "encoding_rs" version = "0.8.30" @@ -422,6 +475,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "firestorm" version = "0.5.0" @@ -794,6 +860,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "idna" version = "0.2.3" @@ -815,6 +887,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.1" @@ -857,6 +938,16 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "local-channel" version = "0.1.2" @@ -978,6 +1069,17 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + [[package]] name = "parking_lot" version = "0.12.0" @@ -985,7 +1087,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.1", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -1045,6 +1161,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1103,6 +1225,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot 0.11.2", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -1174,6 +1307,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot 0.11.2", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1294,6 +1436,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -1358,7 +1509,7 @@ dependencies = [ "memchr", "mio", "once_cell", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1459,6 +1610,22 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1571,6 +1738,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index ddea0ef..4e52b7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,10 @@ edition = "2021" [dependencies] actix-web = "4" yew = { git = "https://github.com/yewstack/yew/", features = ["ssr"] } +diesel = { version = "1.4.8", features = ["sqlite", "r2d2"] } +dotenv = "0.15" +env_logger = "0.9.0" +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +uuid = { version = "0.8", features = ["serde", "v4"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..60a7295 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Actix web + Diesel + Yew SSR + +Basic complete web application with rust. + +## Usage + +### Install SQLite + +```sh +# on OpenSUSE +sudo zypper install sqlite3-devel libsqlite3-0 sqlite3 + +# on Ubuntu +sudo apt-get install libsqlite3-dev sqlite3 + +# on Fedora +sudo dnf install libsqlite3x-devel sqlite3x + +# on macOS (using homebrew) +brew install sqlite3 +``` + +### Initialize SQLite Database + +```sh + +# UNIX +cargo install diesel_cli --no-default-features --features sqlite + +# IF NOT WORK USE THIS + +cargo install diesel_cli --no-default-features --features sqlite-bundled + +echo "DATABASE_URL=db/test.db" > .env +diesel migration run +``` + +There will now be a database file at `./test.db`. + +### Running Server + +```sh +cargo run + +# Started http server: 127.0.0.1:8080 +``` + +### Explore The SQLite DB + +```sh +sqlite3 test.db +``` + +``` +sqlite> .tables +sqlite> SELECT * FROM users; +``` + +## Using Other Databases + +You can find a complete example of Diesel + PostgreSQL at: [https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix) \ No newline at end of file diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2022-03-21-202844_create_users/down.sql b/migrations/2022-03-21-202844_create_users/down.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/migrations/2022-03-21-202844_create_users/down.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/migrations/2022-03-21-202844_create_users/up.sql b/migrations/2022-03-21-202844_create_users/up.sql new file mode 100644 index 0000000..9435d56 --- /dev/null +++ b/migrations/2022-03-21-202844_create_users/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL +); \ No newline at end of file diff --git a/src/controllers/hello_controller.rs b/src/controllers/hello_controller.rs index 0a075cc..32d4c32 100644 --- a/src/controllers/hello_controller.rs +++ b/src/controllers/hello_controller.rs @@ -1,5 +1,4 @@ - -use actix_web::{get, Responder, web, HttpResponse}; +use actix_web::{get, web, HttpResponse, Responder}; use yew::ServerRenderer; use crate::views::components::hello::{Hello, HelloProps}; @@ -7,8 +6,6 @@ use crate::views::components::hello::{Hello, HelloProps}; #[get("/{name}")] async fn index(name: web::Path) -> impl Responder { let name = name.into_inner(); - let renderer = ServerRenderer::::with_props(HelloProps { - name - }); + let renderer = ServerRenderer::::with_props(HelloProps { name }); HttpResponse::Ok().body(renderer.render().await) -} \ No newline at end of file +} diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 5bac290..cc62811 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1 +1,2 @@ -pub mod hello_controller; \ No newline at end of file +pub mod hello_controller; +pub mod users_controller; diff --git a/src/controllers/users_controller.rs b/src/controllers/users_controller.rs new file mode 100644 index 0000000..30cac02 --- /dev/null +++ b/src/controllers/users_controller.rs @@ -0,0 +1,44 @@ +use actix_web::{get, web, Error, HttpResponse}; +use yew::ServerRenderer; + +use crate::{ + models::user::{NewUser, User}, + views::components::{ + user::{UserComponent, UserComponentProps}, + users::{UsersComponent, UsersComponentProps} + }, + DbPool, +}; + +#[get("")] +async fn index(pool: web::Data) -> Result { + let users = web::block(move || { + let conn = pool.get()?; + User::all(&conn) + }) + .await? + .map_err(actix_web::error::ErrorInternalServerError)?; + + let renderer = + ServerRenderer::::with_props(UsersComponentProps { users: users.clone() }); + Ok(HttpResponse::Ok().body(renderer.render().await)) +} + +#[get("")] +async fn create(pool: web::Data) -> Result { + let user = User::new(NewUser { + name: "John".to_string(), + }); + + let u = user.clone(); + web::block(move || { + let conn = pool.get()?; + u.insert(&conn) + }) + .await? + .map_err(actix_web::error::ErrorInternalServerError)?; + + let renderer = + ServerRenderer::::with_props(UserComponentProps { user: user.clone() }); + Ok(HttpResponse::Ok().body(renderer.render().await)) +} diff --git a/src/main.rs b/src/main.rs index d89b9e6..92a96ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,38 @@ +#[macro_use] +extern crate diesel; // Required for schema.rs + pub(self) mod controllers; +pub(self) mod models; +pub(self) mod schema; pub(self) mod views; -use actix_web::{App, HttpServer, web}; +pub(self) type DbPool = r2d2::Pool>; + +use actix_web::{middleware, web, App, HttpServer}; +use diesel::{ + r2d2::{self, ConnectionManager}, + SqliteConnection, +}; #[actix_web::main] -async fn main() -> std::io::Result<()>{ - HttpServer::new(|| { - App::new().service(web::scope("/hello").service(controllers::hello_controller::index)) +async fn main() -> std::io::Result<()> { + dotenv::dotenv().ok(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + // set up database connection pool + let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); + let manager = ConnectionManager::::new(conn_spec); + let pool = r2d2::Pool::builder() + .build(manager) + .expect("Failed to create pool."); + + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(pool.clone())) + .wrap(middleware::Logger::default()) + .service(web::scope("/hello").service(controllers::hello_controller::index)) + .service(web::scope("/users").service(controllers::users_controller::index)) + .service(web::scope("/user").service(controllers::users_controller::create)) }) .bind(("127.0.0.1", 8000))? .run() diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..53afe73 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,3 @@ +pub(self) type DbError = Box; + +pub mod user; diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..42dcd6e --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,64 @@ +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::schema::users; + +use super::DbError; + +#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset, PartialEq)] +pub struct User { + pub id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewUser { + pub name: String, +} + +impl User { + pub fn new(new_user: NewUser) -> Self { + Self { + id: Uuid::new_v4().to_string(), + name: new_user.name, + } + } + + pub fn all(conn: &SqliteConnection) -> Result, DbError> { + users::table.load::(conn).map_err(Into::into) + } + + pub fn find_by_id(conn: &SqliteConnection, id: String) -> Result { + users::table + .filter(users::id.eq(id)) + .first::(conn) + .map_err(Into::into) + } + + pub fn insert(&self, conn: &SqliteConnection) -> Result<(), DbError> { + use crate::schema::users::dsl::*; + + diesel::insert_into(users).values(self).execute(conn)?; + + Ok(()) + } + + pub fn update(&self, conn: &SqliteConnection) -> Result<(), DbError> { + use crate::schema::users::dsl::*; + + diesel::update(users.find(self.id.clone())) + .set(self) + .execute(conn)?; + + Ok(()) + } + + pub fn delete(&self, conn: &SqliteConnection) -> Result<(), DbError> { + use crate::schema::users::dsl::*; + + diesel::delete(users.filter(id.eq(self.id.clone()))).execute(conn)?; + + Ok(()) + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..51aa40b --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,6 @@ +table! { + users (id) { + id -> Text, + name -> Text, + } +} diff --git a/src/views/components/mod.rs b/src/views/components/mod.rs index 229ec9b..fdf346b 100644 --- a/src/views/components/mod.rs +++ b/src/views/components/mod.rs @@ -1 +1,3 @@ -pub mod hello; \ No newline at end of file +pub mod hello; +pub mod user; +pub mod users; \ No newline at end of file diff --git a/src/views/components/user.rs b/src/views/components/user.rs new file mode 100644 index 0000000..c57b009 --- /dev/null +++ b/src/views/components/user.rs @@ -0,0 +1,18 @@ +use yew::prelude::*; + +use crate::models::user::User; + +#[derive(Properties, PartialEq)] +pub struct UserComponentProps { + pub user: User, +} + +#[function_component] +pub fn UserComponent(props: &UserComponentProps) -> Html { + html! { +
+

{ props.user.clone().id }

+

{ props.user.clone().name }

+
+ } +} \ No newline at end of file diff --git a/src/views/components/users.rs b/src/views/components/users.rs new file mode 100644 index 0000000..d692ace --- /dev/null +++ b/src/views/components/users.rs @@ -0,0 +1,21 @@ +use yew::prelude::*; + +use crate::models::user::User; +use crate::views::components::user::UserComponent; + +#[derive(Properties, PartialEq)] +pub struct UsersComponentProps { + pub users: Vec, +} + +#[function_component] +pub fn UsersComponent(props: &UsersComponentProps) -> Html { + let users = props.users.clone().iter().map(|user| html! { + + }).collect::(); + html! { +
+ { users } +
+ } +} \ No newline at end of file