The point where I realise that the tutorial is incomplete and I can't easily fill in the gaps.

tutorial
Gandalf 2023-06-05 12:02:35 +02:00
parent 51a4c78bd5
commit 0f432d3e7e
11 changed files with 2324 additions and 8 deletions

1997
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,3 @@
[package]
name = "HambiMap"
version = "0.1.0"
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
edition = "2018"
[workspace] [workspace]
members = [ members = [

View File

@ -7,3 +7,5 @@ I'll use this branch to follow [this tutorial](https://blog.logrocket.com/full-s
This uses postgresql as database, my original plan was sqlite. Let's see. 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. 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.

View File

@ -9,6 +9,8 @@ tokio = { version = "=1.6.1", features = ["macros", "rt-multi-thread"] }
warp = "=0.3.1" warp = "=0.3.1"
mobc = "=0.7.2" mobc = "=0.7.2"
mobc-postgres = { version = "=0.7.0", features = ["with-chrono-0_4", "with-serde_json-1"] } 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 = {version = "=1.0.126", features = ["derive"] }
serde_json = "=1.0.64" serde_json = "=1.0.64"
thiserror = "=1.0.24" thiserror = "=1.0.24"

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)?,
)))
}

View File

@ -1,3 +1,74 @@
fn main() { mod db;
println!("Hello, world!"); 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())
} }

View File

@ -5,3 +5,66 @@ mod tests {
assert_eq!(2 + 2, 4); 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,
}
}
}