Compare commits

...

2 commits

15 changed files with 2394 additions and 4 deletions

1997
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[workspace]
members = [
"backend",
"frontend",
#Internal
"common"
]

View file

@ -1,3 +1,11 @@
# HambiMap # HambiMap
Code for a Website that collects stories and photos from 11 years of Hambach Forest occupation, with the goal of producing info tables to put up in the forest Code for a Website that collects stories and photos from 11 years of Hambach Forest occupation, with the goal of producing info tables to put up in the forest
## Tutorial branch
I'll use this branch to follow [this tutorial](https://blog.logrocket.com/full-stack-rust-a-complete-tutorial-with-examples/)
This uses postgresql as database, my original plan was sqlite. Let's see.
Also I'm not sure if wasm is the frontend I want.
There are crates used that require a newer rust than debian stable can provide.

17
backend/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "backend"
version = "0.1.0"
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
edition = "2018"
[dependencies]
tokio = { version = "=1.6.1", features = ["macros", "rt-multi-thread"] }
warp = "=0.3.1"
mobc = "=0.7.2"
mobc-postgres = { version = "=0.7.0", features = ["with-chrono-0_4", "with-serde_json-1"] }
# tokio-postgres = "=0.7.0"
hyper = "=0.14.0"
serde = {version = "=1.0.126", features = ["derive"] }
serde_json = "=1.0.64"
thiserror = "=1.0.24"
common = { version = "0.1.0", path = "../common" }

16
backend/src/db/db.sql Normal file
View file

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS owner
(
id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS pet
(
id SERIAL PRIMARY KEY NOT NULL,
owner_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
animal_type VARCHAR(255) NOT NULL,
color VARCHAR(255),
CONSTRAINT fk_pet_owner_id FOREIGN KEY (owner_id) REFERENCES pet(id)
);

30
backend/src/db/mod.rs Normal file
View file

@ -0,0 +1,30 @@
type Result<T> = std::result::Result<T, error::Error>;
const DB_POOL_MAX_OPEN: u64 = 32;
const DB_POOL_MAX_IDLE: u64 = 8;
const DB_POOL_TIMEOUT_SECONDS: u64 = 15;
const INIT_SQL: &str = "./db.sql";
pub async fn init_db(db_pool: &DBPool) -> Result<()> {
let init_file = fs::read_to_string(INIT_SQL)?;
let con = get_db_con(db_pool).await?;
con.batch_execute(init_file.as_str())
.await
.map_err(DBInitError)?;
Ok(())
}
pub async fn get_db_con(db_pool: &DBPool) -> Result<DBCon> {
db_pool.get().await.map_err(DBPoolError)
}
pub fn create_pool() -> std::result::Result<DBPool, mobc::Error<Error>> {
let config = Config::from_str("postgres://postgres@127.0.0.1:7878/postgres")?;
let manager = PgConnectionManager::new(config, NoTls);
Ok(Pool::builder()
.max_open(DB_POOL_MAX_OPEN)
.max_idle(DB_POOL_MAX_IDLE)
.get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)))
.build(manager))
}

37
backend/src/db/owner.rs Normal file
View file

@ -0,0 +1,37 @@
pub const TABLE: &str = "owner";
const SELECT_FIELDS: &str = "id, name";
pub async fn fetch(db_pool: &DBPool) -> Result<Vec<Owner>> {
let con = get_db_con(db_pool).await?;
let query = format!("SELECT {} FROM {}", SELECT_FIELDS, TABLE);
let rows = con.query(query.as_str(), &[]).await.map_err(DBQueryError)?;
Ok(rows.iter().map(|r| row_to_owner(&r)).collect())
}
pub async fn fetch_one(db_pool: &DBPool, id: i32) -> Result<Owner> {
let con = get_db_con(db_pool).await?;
let query = format!("SELECT {} FROM {} WHERE id = $1", SELECT_FIELDS, TABLE);
let row = con
.query_one(query.as_str(), &[&id])
.await
.map_err(DBQueryError)?;
Ok(row_to_owner(&row))
}
pub async fn create(db_pool: &DBPool, body: OwnerRequest) -> Result<Owner> {
let con = get_db_con(db_pool).await?;
let query = format!("INSERT INTO {} (name) VALUES ($1) RETURNING *", TABLE);
let row = con
.query_one(query.as_str(), &[&body.name])
.await
.map_err(DBQueryError)?;
Ok(row_to_owner(&row))
}
fn row_to_owner(row: &Row) -> Owner {
let id: i32 = row.get(0);
let name: String = row.get(1);
Owner { id, name }
}

55
backend/src/db/pet.rs Normal file
View file

@ -0,0 +1,55 @@
pub const TABLE: &str = "pet";
const SELECT_FIELDS: &str = "id, owner_id, name, animal_type, color";
pub async fn fetch(db_pool: &DBPool, owner_id: i32) -> Result<Vec<Pet>> {
let con = get_db_con(db_pool).await?;
let query = format!(
"SELECT {} FROM {} WHERE owner_id = $1",
SELECT_FIELDS, TABLE
);
let rows = con
.query(query.as_str(), &[&owner_id])
.await
.map_err(DBQueryError)?;
Ok(rows.iter().map(|r| row_to_pet(&r)).collect())
}
pub async fn create(db_pool: &DBPool, owner_id: i32, body: PetRequest) -> Result<Pet> {
let con = get_db_con(db_pool).await?;
let query = format!(
"INSERT INTO {} (name, owner_id, animal_type, color) VALUES ($1, $2, $3, $4) RETURNING *",
TABLE
);
let row = con
.query_one(
query.as_str(),
&[&body.name, &owner_id, &body.animal_type, &body.color],
)
.await
.map_err(DBQueryError)?;
Ok(row_to_pet(&row))
}
pub async fn delete(db_pool: &DBPool, owner_id: i32, id: i32) -> Result<u64> {
let con = get_db_con(db_pool).await?;
let query = format!("DELETE FROM {} WHERE id = $1 AND owner_id = $2", TABLE);
con.execute(query.as_str(), &[&id, &owner_id])
.await
.map_err(DBQueryError)
}
fn row_to_pet(row: &Row) -> Pet {
let id: i32 = row.get(0);
let owner_id: i32 = row.get(1);
let name: String = row.get(2);
let animal_type: String = row.get(3);
let color: Option<String> = row.get(4);
Pet {
id,
name,
owner_id,
animal_type,
color,
}
}

49
backend/src/handler.rs Normal file
View file

@ -0,0 +1,49 @@
pub async fn list_pets_handler(owner_id: i32, db_pool: DBPool) -> Result<impl Reply> {
let pets = db::pet::fetch(&db_pool, owner_id)
.await
.map_err(reject::custom)?;
Ok(json::<Vec<_>>(
&pets.into_iter().map(PetResponse::of).collect(),
))
}
pub async fn create_pet_handler(
owner_id: i32,
body: PetRequest,
db_pool: DBPool,
) -> Result<impl Reply> {
Ok(json(&PetResponse::of(
db::pet::create(&db_pool, owner_id, body)
.await
.map_err(reject::custom)?,
)))
}
pub async fn delete_pet_handler(owner_id: i32, id: i32, db_pool: DBPool) -> Result<impl Reply> {
db::pet::delete(&db_pool, owner_id, id)
.await
.map_err(reject::custom)?;
Ok(StatusCode::OK)
}
pub async fn list_owners_handler(db_pool: DBPool) -> Result<impl Reply> {
let owners = db::owner::fetch(&db_pool).await.map_err(reject::custom)?;
Ok(json::<Vec<_>>(
&owners.into_iter().map(OwnerResponse::of).collect(),
))
}
pub async fn fetch_owner_handler(id: i32, db_pool: DBPool) -> Result<impl Reply> {
let owner = db::owner::fetch_one(&db_pool, id)
.await
.map_err(reject::custom)?;
Ok(json(&OwnerResponse::of(owner)))
}
pub async fn create_owner_handler(body: OwnerRequest, db_pool: DBPool) -> Result<impl Reply> {
Ok(json(&OwnerResponse::of(
db::owner::create(&db_pool, body)
.await
.map_err(reject::custom)?,
)))
}

74
backend/src/main.rs Normal file
View file

@ -0,0 +1,74 @@
mod db;
mod error;
mod handler;
type Result<T> = std::result::Result<T, Rejection>;
type DBCon = Connection<PgConnectionManager<NoTls>>;
type DBPool = Pool<PgConnectionManager<NoTls>>;
#[tokio::main]
async fn main() {
let db_pool = db::create_pool().expect("database pool can be created");
db::init_db(&db_pool)
.await
.expect("database can be initialized");
let pet = warp::path!("owner" / i32 / "pet");
let pet_param = warp::path!("owner" / i32 / "pet" / i32);
let owner = warp::path("owner");
let pet_routes = pet
.and(warp::get())
.and(with_db(db_pool.clone()))
.and_then(handler::list_pets_handler)
.or(pet
.and(warp::post())
.and(warp::body::json())
.and(with_db(db_pool.clone()))
.and_then(handler::create_pet_handler))
.or(pet_param
.and(warp::delete())
.and(with_db(db_pool.clone()))
.and_then(handler::delete_pet_handler));
let owner_routes = owner
.and(warp::get())
.and(warp::path::param())
.and(with_db(db_pool.clone()))
.and_then(handler::fetch_owner_handler)
.or(owner
.and(warp::get())
.and(with_db(db_pool.clone()))
.and_then(handler::list_owners_handler))
.or(owner
.and(warp::post())
.and(warp::body::json())
.and(with_db(db_pool.clone()))
.and_then(handler::create_owner_handler));
let routes = pet_routes
.or(owner_routes)
.recover(error::handle_rejection)
.with(
warp::cors()
.allow_credentials(true)
.allow_methods(&[
Method::OPTIONS,
Method::GET,
Method::POST,
Method::DELETE,
Method::PUT,
])
.allow_headers(vec![header::CONTENT_TYPE, header::ACCEPT])
.expose_headers(vec![header::LINK])
.max_age(300)
.allow_any_origin(),
);
warp::serve(routes).run(([127, 0, 0, 1], 8000)).await;
}
fn with_db(db_pool: DBPool) -> impl Filter<Extract = (DBPool,), Error = Infallible> + Clone {
warp::any().map(move || db_pool.clone())
}

8
common/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "common"
version = "0.1.0"
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
edition = "2018"
[dependencies]
serde = {version = "=1.0.126", features = ["derive"] }

70
common/src/lib.rs Normal file
View file

@ -0,0 +1,70 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Clone, PartialEq, Debug)]
pub struct Owner {
pub id: i32,
pub name: String,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct OwnerRequest {
pub name: String,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct OwnerResponse {
pub id: i32,
pub name: String,
}
impl OwnerResponse {
pub fn of(owner: Owner) -> OwnerResponse {
OwnerResponse {
id: owner.id,
name: owner.name,
}
}
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
pub struct Pet {
pub id: i32,
pub name: String,
pub owner_id: i32,
pub animal_type: String,
pub color: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct PetRequest {
pub name: String,
pub animal_type: String,
pub color: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct PetResponse {
pub id: i32,
pub name: String,
pub animal_type: String,
pub color: Option<String>,
}
impl PetResponse {
pub fn of(pet: Pet) -> PetResponse {
PetResponse {
id: pet.id,
name: pet.name,
animal_type: pet.animal_type,
color: pet.color,
}
}
}

16
frontend/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "frontend"
version = "0.1.0"
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = "0.18"
wasm-bindgen = "0.2.67"
serde_json = "1"
serde = {version = "=1.0.126", features = ["derive"] }
anyhow = "1"
yew-router = "0.15.0"
common = { version = "0.1.0", path = "../common" }

7
frontend/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}