diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 0000000..330747b --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,6 @@ +use sqlx::PgPool; + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..0861c5f --- /dev/null +++ b/src/database.rs @@ -0,0 +1,29 @@ +use sqlx::PgPool; +use std::{env, fs}; + +pub fn database_url() -> String { + let _ = dotenvy::dotenv(); + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + env::var("POSTGRES_USER").unwrap(), + env::var("POSTGRES_PASSWORD").unwrap(), + env::var("POSTGRES_HOST").unwrap(), + env::var("POSTGRES_PORT").unwrap(), + env::var("POSTGRES_DB").unwrap(), + ); + if database_url != env::var("DATABASE_URL").unwrap() { + panic!("DATABASE_URL is not set correctly"); + } + println!("DATABASE_URL={database_url}"); + database_url +} + +pub async fn create_table(pool: &PgPool) { + let sql_up = + fs::read_to_string("migrations/create_tables.sql").expect("Failed to read SQL file"); + + sqlx::query(&sql_up) + .execute(pool) + .await + .expect("Could not create database tables"); +} diff --git a/src/main.rs b/src/main.rs index 40a959f..af2a4e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,147 +1,24 @@ -use axum::{ - Json, - extract::{Query, State}, - response::IntoResponse, - routing::{get, post}, -}; use sqlx::PgPool; -use std::{env, fs}; use tokio::net::TcpListener; -#[derive(Clone)] -pub struct AppState { - pub pool: PgPool, -} +mod app_state; +mod database; +mod router; -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct Game { - id: i32, - name: String, - publishing_house: String, - developer: String, - description: String, - multiplayer: bool, - is_free_to_play: bool, - // - // release_date: i32, // date? - // rating: Option, // enum? - // cover_image_url: Option, - // - // platforms: Vec, // enum - // genre: Vec, // enum? - // age_rating: Option, // enum? - // game_engine: Option, // enum? - // tags: Option>, -} - -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct CreateGame { - name: String, - publishing_house: String, - developer: String, - description: String, - multiplayer: bool, - is_free_to_play: bool, -} - -#[derive(serde::Deserialize)] -pub struct GetGameQuery { - pub id: Option, -} - -fn database_url() -> String { - let _ = dotenvy::dotenv(); - let database_url = format!( - "postgres://{}:{}@{}:{}/{}", - env::var("POSTGRES_USER").unwrap(), - env::var("POSTGRES_PASSWORD").unwrap(), - env::var("POSTGRES_HOST").unwrap(), - env::var("POSTGRES_PORT").unwrap(), - env::var("POSTGRES_DB").unwrap(), - ); - if database_url != env::var("DATABASE_URL").unwrap() { - panic!("DATABASE_URL is not set correctly"); - } - println!("DATABASE_URL={database_url}"); - database_url -} - -async fn create_table(pool: &PgPool) { - let sql_up = - fs::read_to_string("migrations/create_tables.sql").expect("Failed to read SQL file"); - - sqlx::query(&sql_up) - .execute(pool) - .await - .expect("Could not create database tables"); -} - -fn router(state: AppState) -> axum::Router { - axum::Router::new() - .route("/api/games", get(games_list).post(insert_game)) - .route("/api/games/{id}", post(insert_game).delete(delete_game)) - .with_state(state) -} +use app_state::AppState; #[tokio::main] async fn main() { let state = AppState { - pool: PgPool::connect(&database_url()) + pool: PgPool::connect(&database::database_url()) .await .expect("Could not connect to database"), }; - create_table(&state.pool); + database::create_table(&state.pool); let addr = "127.0.0.1:8000"; let listener = TcpListener::bind(addr).await.unwrap(); println!("Listening at {addr}"); - axum::serve(listener, router(state)).await.unwrap(); -} - -async fn games_list(State(state): State) -> impl IntoResponse { - let games: Vec = sqlx::query_as!(Game, "SELECT * FROM games") - .fetch_all(&state.pool) - .await - .unwrap(); - - Json(games) -} - -async fn insert_game( - State(state): State, - Query(query): Query, - Json(game): Json, -) -> impl IntoResponse { - let sql_up = if query.id.is_some() { - "UPDATE games SET name=$1, publishing_house=$2, developer=$3, description=$4, multiplayer=$5, is_free_to_play=$6 WHERE id=$7" - } else { - "INSERT INTO games(name, publishing_house, developer, description, multiplayer, is_free_to_play) VALUES ($1, $2, $3, $4, $5, $6)" - }; - - let mut sql_query = sqlx::query(sql_up) - .bind(game.name) - .bind(game.publishing_house) - .bind(game.developer) - .bind(game.description) - .bind(game.multiplayer) - .bind(game.is_free_to_play); - - if let Some(id) = query.id { - sql_query = sql_query.bind(id); - } - sql_query.execute(&state.pool).await.unwrap(); - - axum::http::StatusCode::CREATED -} - -use axum::extract::Path; - -async fn delete_game(State(state): State, Path(id): Path) -> impl IntoResponse { - sqlx::query!("DELETE FROM games WHERE id = $1", id) - .execute(&state.pool) - .await - .unwrap(); - - axum::http::StatusCode::OK + axum::serve(listener, router::router(state)).await.unwrap(); } diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..f753f25 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,19 @@ +use axum::routing::{get, post}; + +pub mod funcs; +pub mod game; + +use crate::app_state::AppState; + +pub fn router(state: AppState) -> axum::Router { + axum::Router::new() + .route( + "/api/games", + get(funcs::games_list).post(funcs::insert_game), + ) + .route( + "/api/games/{id}", + post(funcs::insert_game).delete(funcs::delete_game), + ) + .with_state(state) +} diff --git a/src/router/funcs.rs b/src/router/funcs.rs new file mode 100644 index 0000000..3b96c06 --- /dev/null +++ b/src/router/funcs.rs @@ -0,0 +1,53 @@ +use axum::{ + Json, + extract::{Path, Query, State}, + response::IntoResponse, +}; + +use super::game::{CreateGame, Game, GetGameQuery}; +use crate::app_state::AppState; + +pub async fn games_list(State(state): State) -> impl IntoResponse { + let games: Vec = sqlx::query_as!(Game, "SELECT * FROM games") + .fetch_all(&state.pool) + .await + .unwrap(); + + Json(games) +} + +pub async fn insert_game( + State(state): State, + Query(query): Query, + Json(game): Json, +) -> impl IntoResponse { + let sql_up = if query.id.is_some() { + "UPDATE games SET name=$1, publishing_house=$2, developer=$3, description=$4, multiplayer=$5, is_free_to_play=$6 WHERE id=$7" + } else { + "INSERT INTO games(name, publishing_house, developer, description, multiplayer, is_free_to_play) VALUES ($1, $2, $3, $4, $5, $6)" + }; + + let mut sql_query = sqlx::query(sql_up) + .bind(game.name) + .bind(game.publishing_house) + .bind(game.developer) + .bind(game.description) + .bind(game.multiplayer) + .bind(game.is_free_to_play); + + if let Some(id) = query.id { + sql_query = sql_query.bind(id); + } + sql_query.execute(&state.pool).await.unwrap(); + + axum::http::StatusCode::CREATED +} + +pub async fn delete_game(State(state): State, Path(id): Path) -> impl IntoResponse { + sqlx::query!("DELETE FROM games WHERE id = $1", id) + .execute(&state.pool) + .await + .unwrap(); + + axum::http::StatusCode::OK +} diff --git a/src/router/game.rs b/src/router/game.rs new file mode 100644 index 0000000..be80ddd --- /dev/null +++ b/src/router/game.rs @@ -0,0 +1,35 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Game { + pub id: i32, + pub name: String, + pub publishing_house: String, + pub developer: String, + pub description: String, + pub multiplayer: bool, + pub is_free_to_play: bool, + // + // release_date: i32, // date? + // rating: Option, // enum? + // cover_image_url: Option, + // + // platforms: Vec, // enum + // genre: Vec, // enum? + // age_rating: Option, // enum? + // game_engine: Option, // enum? + // tags: Option>, +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct CreateGame { + pub name: String, + pub publishing_house: String, + pub developer: String, + pub description: String, + pub multiplayer: bool, + pub is_free_to_play: bool, +} + +#[derive(serde::Deserialize)] +pub struct GetGameQuery { + pub id: Option, +}