diff --git a/backend-actix/build.rs b/backend-actix/build.rs index 5b33931..21e75ca 100644 --- a/backend-actix/build.rs +++ b/backend-actix/build.rs @@ -2,6 +2,7 @@ use std::{fs::{exists, read_dir, remove_dir_all, File}, io::Write, process::Comm fn main() { + println!("cargo::rerun-if-changed=gamenight-api.yaml"); if exists("src/models").unwrap() { remove_dir_all("src/models").unwrap(); diff --git a/backend-actix/gamenight-api.yaml b/backend-actix/gamenight-api.yaml index 4e6259c..c9eaa80 100644 --- a/backend-actix/gamenight-api.yaml +++ b/backend-actix/gamenight-api.yaml @@ -38,7 +38,6 @@ paths: parameters: [] security: - JWT-Auth: [] - /user: post: summary: '' @@ -146,7 +145,7 @@ paths: post: responses: '200': - description: OK + description: "OK" '401': $ref: '#/components/responses/FailureResponse' '422': @@ -155,6 +154,42 @@ paths: $ref: '#/components/requestBodies/LeaveGamenight' security: - JWT-Auth: [] + /games: + get: + responses: + '200': + $ref: '#/components/responses/GamesResponse' + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + security: + - JWT-Auth: [] + /game: + get: + responses: + '200': + $ref: '#/components/responses/GameResponse' + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + requestBody: + $ref: '#/components/requestBodies/GetGameRequest' + security: + - JWT-Auth: [] + post: + responses: + '200': + description: "OK" + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + requestBody: + $ref: '#/components/requestBodies/AddGameRequest' + security: + - JWT-Auth: [] components: schemas: @@ -256,6 +291,14 @@ components: type: string required: - gamenight_id + GameId: + title: GameId + type: object + properties: + game_id: + type: string + required: + - game_id GetGamenightRequestBody: type: object properties: @@ -273,6 +316,24 @@ components: required: - id - username + Game: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + AddGameRequestBody: + type: object + properties: + name: + type: string + required: + - name + requestBodies: LoginRequest: content: @@ -314,6 +375,16 @@ components: application/json: schema: $ref: '#/components/schemas/GamenightId' + GetGameRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/GameId' + AddGameRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/AddGameRequestBody' responses: TokenResponse: description: Example response @@ -353,6 +424,20 @@ components: application/json: schema: $ref: '#/components/schemas/User' + GamesResponse: + description: A list of Games in this gamenight instance. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Game' + GameResponse: + description: A game. + content: + application/json: + schema: + $ref: '#/components/schemas/Game' securitySchemes: JWT-Auth: type: http diff --git a/backend-actix/src/main.rs b/backend-actix/src/main.rs index 8bd35a7..e5fa859 100644 --- a/backend-actix/src/main.rs +++ b/backend-actix/src/main.rs @@ -48,6 +48,8 @@ async fn main() -> std::io::Result<()> { .service(post_join_gamenight) .service(post_leave_gamenight) .service(get_get_participants) + .service(get_games) + .service(post_game) }) .bind(("::1", 8080))? .run() diff --git a/backend-actix/src/request/game.rs b/backend-actix/src/request/game.rs new file mode 100644 index 0000000..5b353e6 --- /dev/null +++ b/backend-actix/src/request/game.rs @@ -0,0 +1,39 @@ +use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder}; +use gamenight_database::{game::insert_game, DbPool, GetConnection}; +use uuid::Uuid; + +use crate::{models::{add_game_request_body::AddGameRequestBody, game::Game}, request::{authorization::AuthUser, error::ApiError}}; + +#[get("/games")] +pub async fn get_games(pool: web::Data, _user: AuthUser) -> Result { + let mut conn = pool.get_conn(); + let games: Vec = gamenight_database::games(&mut conn)?; + let model: Vec = games.iter().map(|x| { + Game { + id: x.id.to_string(), + name: x.name.clone(), + }} + ).collect(); + + Ok(HttpResponse::Ok() + .content_type(ContentType::json()) + .body(serde_json::to_string(&model)?) + ) +} + +impl From for gamenight_database::game::Game { + fn from(value: AddGameRequestBody) -> Self { + Self { + id: Uuid::new_v4(), + name: value.name + } + } +} + +#[post("/game")] +pub async fn post_game(pool: web::Data, _user: AuthUser, game_data: web::Json) -> Result { + let mut conn = pool.get_conn(); + insert_game(&mut conn, game_data.0.into())?; + + Ok(HttpResponse::Ok()) +} \ No newline at end of file diff --git a/backend-actix/src/request/mod.rs b/backend-actix/src/request/mod.rs index 6a16ca0..2b1aaf0 100644 --- a/backend-actix/src/request/mod.rs +++ b/backend-actix/src/request/mod.rs @@ -5,6 +5,7 @@ mod error; mod authorization; mod join_gamenight; mod participant_handlers; +mod game; pub use user_handlers::login; pub use user_handlers::refresh; @@ -17,3 +18,5 @@ pub use user_handlers::get_user_unauthenticated; pub use join_gamenight::post_join_gamenight; pub use join_gamenight::post_leave_gamenight; pub use participant_handlers::get_get_participants; +pub use game::get_games; +pub use game::post_game; diff --git a/gamenight-api-client-rs/.openapi-generator/FILES b/gamenight-api-client-rs/.openapi-generator/FILES index e0a799e..46428cd 100644 --- a/gamenight-api-client-rs/.openapi-generator/FILES +++ b/gamenight-api-client-rs/.openapi-generator/FILES @@ -2,9 +2,12 @@ .travis.yml Cargo.toml README.md +docs/AddGameRequestBody.md docs/AddGamenightRequestBody.md docs/DefaultApi.md docs/Failure.md +docs/Game.md +docs/GameId.md docs/Gamenight.md docs/GamenightId.md docs/GetGamenightRequestBody.md @@ -19,8 +22,11 @@ src/apis/configuration.rs src/apis/default_api.rs src/apis/mod.rs src/lib.rs +src/models/add_game_request_body.rs src/models/add_gamenight_request_body.rs src/models/failure.rs +src/models/game.rs +src/models/game_id.rs src/models/gamenight.rs src/models/gamenight_id.rs src/models/get_gamenight_request_body.rs diff --git a/gamenight-api-client-rs/README.md b/gamenight-api-client-rs/README.md index 466608d..f52ac51 100644 --- a/gamenight-api-client-rs/README.md +++ b/gamenight-api-client-rs/README.md @@ -27,6 +27,9 @@ All URIs are relative to *http://localhost:8080* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*DefaultApi* | [**game_get**](docs/DefaultApi.md#game_get) | **GET** /game | +*DefaultApi* | [**game_post**](docs/DefaultApi.md#game_post) | **POST** /game | +*DefaultApi* | [**games_get**](docs/DefaultApi.md#games_get) | **GET** /games | *DefaultApi* | [**get_gamenight**](docs/DefaultApi.md#get_gamenight) | **GET** /gamenight | *DefaultApi* | [**get_gamenights**](docs/DefaultApi.md#get_gamenights) | **GET** /gamenights | Get a all gamenights *DefaultApi* | [**get_token**](docs/DefaultApi.md#get_token) | **GET** /token | @@ -41,8 +44,11 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [AddGameRequestBody](docs/AddGameRequestBody.md) - [AddGamenightRequestBody](docs/AddGamenightRequestBody.md) - [Failure](docs/Failure.md) + - [Game](docs/Game.md) + - [GameId](docs/GameId.md) - [Gamenight](docs/Gamenight.md) - [GamenightId](docs/GamenightId.md) - [GetGamenightRequestBody](docs/GetGamenightRequestBody.md) diff --git a/gamenight-api-client-rs/build.rs b/gamenight-api-client-rs/build.rs index a950cd5..03784c1 100644 --- a/gamenight-api-client-rs/build.rs +++ b/gamenight-api-client-rs/build.rs @@ -1,6 +1,7 @@ use std::process::Command; fn main() { + println!("cargo::rerun-if-changed=../backend-actix/gamenight-api.yaml"); let _ = Command::new("openapi-generator") .args(["generate", "-i", "../backend-actix/gamenight-api.yaml", "-g", "rust", "--additional-properties=withSeparateModelsAndApi=true,modelPackage=gamenight_model,apiPackage=gamenight_api,packageName=gamenight-api-client-rs,packageVersion=0.1.0"]) diff --git a/gamenight-api-client-rs/docs/AddGame.md b/gamenight-api-client-rs/docs/AddGame.md new file mode 100644 index 0000000..cc252ac --- /dev/null +++ b/gamenight-api-client-rs/docs/AddGame.md @@ -0,0 +1,11 @@ +# AddGame + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/gamenight-api-client-rs/docs/AddGameRequestBody.md b/gamenight-api-client-rs/docs/AddGameRequestBody.md new file mode 100644 index 0000000..42c31bf --- /dev/null +++ b/gamenight-api-client-rs/docs/AddGameRequestBody.md @@ -0,0 +1,11 @@ +# AddGameRequestBody + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/gamenight-api-client-rs/docs/DefaultApi.md b/gamenight-api-client-rs/docs/DefaultApi.md index 5dbcd02..9917b54 100644 --- a/gamenight-api-client-rs/docs/DefaultApi.md +++ b/gamenight-api-client-rs/docs/DefaultApi.md @@ -4,6 +4,9 @@ All URIs are relative to *http://localhost:8080* Method | HTTP request | Description ------------- | ------------- | ------------- +[**game_get**](DefaultApi.md#game_get) | **GET** /game | +[**game_post**](DefaultApi.md#game_post) | **POST** /game | +[**games_get**](DefaultApi.md#games_get) | **GET** /games | [**get_gamenight**](DefaultApi.md#get_gamenight) | **GET** /gamenight | [**get_gamenights**](DefaultApi.md#get_gamenights) | **GET** /gamenights | Get a all gamenights [**get_token**](DefaultApi.md#get_token) | **GET** /token | @@ -17,6 +20,87 @@ Method | HTTP request | Description +## game_get + +> models::Game game_get(game_id) + + +### Parameters + + +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | ------------- +**game_id** | Option<[**GameId**](GameId.md)> | | | + +### Return type + +[**models::Game**](Game.md) + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +## game_post + +> game_post(add_game_request_body) + + +### Parameters + + +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | ------------- +**add_game_request_body** | Option<[**AddGameRequestBody**](AddGameRequestBody.md)> | | | + +### Return type + + (empty response body) + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +## games_get + +> Vec games_get() + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**Vec**](Game.md) + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + ## get_gamenight > models::Gamenight get_gamenight(get_gamenight_request_body) diff --git a/gamenight-api-client-rs/docs/Game.md b/gamenight-api-client-rs/docs/Game.md new file mode 100644 index 0000000..36784e1 --- /dev/null +++ b/gamenight-api-client-rs/docs/Game.md @@ -0,0 +1,12 @@ +# Game + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **String** | | +**name** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/gamenight-api-client-rs/docs/GameId.md b/gamenight-api-client-rs/docs/GameId.md new file mode 100644 index 0000000..aa00a6f --- /dev/null +++ b/gamenight-api-client-rs/docs/GameId.md @@ -0,0 +1,11 @@ +# GameId + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**game_id** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/gamenight-api-client-rs/src/apis/default_api.rs b/gamenight-api-client-rs/src/apis/default_api.rs index f59fcef..ce55589 100644 --- a/gamenight-api-client-rs/src/apis/default_api.rs +++ b/gamenight-api-client-rs/src/apis/default_api.rs @@ -15,6 +15,33 @@ use crate::{apis::ResponseContent, models}; use super::{Error, configuration, ContentType}; +/// struct for typed errors of method [`game_get`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GameGetError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`game_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GamePostError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`games_get`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GamesGetError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`get_gamenight`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -104,6 +131,112 @@ pub enum UserGetError { } +pub async fn game_get(configuration: &configuration::Configuration, game_id: Option) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_game_id = game_id; + + let uri_str = format!("{}/game", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_game_id); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Game`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Game`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + +pub async fn game_post(configuration: &configuration::Configuration, add_game_request_body: Option) -> Result<(), Error> { + // add a prefix to parameters to efficiently prevent name collisions + let p_add_game_request_body = add_game_request_body; + + let uri_str = format!("{}/game", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_add_game_request_body); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + if !status.is_client_error() && !status.is_server_error() { + Ok(()) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + +pub async fn games_get(configuration: &configuration::Configuration, ) -> Result, Error> { + + let uri_str = format!("{}/games", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `Vec<models::Game>`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `Vec<models::Game>`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + pub async fn get_gamenight(configuration: &configuration::Configuration, get_gamenight_request_body: Option) -> Result> { // add a prefix to parameters to efficiently prevent name collisions let p_get_gamenight_request_body = get_gamenight_request_body; diff --git a/gamenight-api-client-rs/src/models/add_game.rs b/gamenight-api-client-rs/src/models/add_game.rs new file mode 100644 index 0000000..4fa698f --- /dev/null +++ b/gamenight-api-client-rs/src/models/add_game.rs @@ -0,0 +1,27 @@ +/* + * Gamenight + * + * Api specifaction for a Gamenight server + * + * The version of the OpenAPI document: 1.0 + * Contact: dennis@brentj.es + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AddGame { + #[serde(rename = "name")] + pub name: String, +} + +impl AddGame { + pub fn new(name: String) -> AddGame { + AddGame { + name, + } + } +} + diff --git a/gamenight-api-client-rs/src/models/add_game_request_body.rs b/gamenight-api-client-rs/src/models/add_game_request_body.rs new file mode 100644 index 0000000..9d3f38c --- /dev/null +++ b/gamenight-api-client-rs/src/models/add_game_request_body.rs @@ -0,0 +1,27 @@ +/* + * Gamenight + * + * Api specifaction for a Gamenight server + * + * The version of the OpenAPI document: 1.0 + * Contact: dennis@brentj.es + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AddGameRequestBody { + #[serde(rename = "name")] + pub name: String, +} + +impl AddGameRequestBody { + pub fn new(name: String) -> AddGameRequestBody { + AddGameRequestBody { + name, + } + } +} + diff --git a/gamenight-api-client-rs/src/models/game.rs b/gamenight-api-client-rs/src/models/game.rs new file mode 100644 index 0000000..d684f46 --- /dev/null +++ b/gamenight-api-client-rs/src/models/game.rs @@ -0,0 +1,30 @@ +/* + * Gamenight + * + * Api specifaction for a Gamenight server + * + * The version of the OpenAPI document: 1.0 + * Contact: dennis@brentj.es + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Game { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "name")] + pub name: String, +} + +impl Game { + pub fn new(id: String, name: String) -> Game { + Game { + id, + name, + } + } +} + diff --git a/gamenight-api-client-rs/src/models/game_id.rs b/gamenight-api-client-rs/src/models/game_id.rs new file mode 100644 index 0000000..001d2f2 --- /dev/null +++ b/gamenight-api-client-rs/src/models/game_id.rs @@ -0,0 +1,27 @@ +/* + * Gamenight + * + * Api specifaction for a Gamenight server + * + * The version of the OpenAPI document: 1.0 + * Contact: dennis@brentj.es + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GameId { + #[serde(rename = "game_id")] + pub game_id: String, +} + +impl GameId { + pub fn new(game_id: String) -> GameId { + GameId { + game_id, + } + } +} + diff --git a/gamenight-api-client-rs/src/models/mod.rs b/gamenight-api-client-rs/src/models/mod.rs index c6d40a2..20b11a6 100644 --- a/gamenight-api-client-rs/src/models/mod.rs +++ b/gamenight-api-client-rs/src/models/mod.rs @@ -1,7 +1,13 @@ +pub mod add_game_request_body; +pub use self::add_game_request_body::AddGameRequestBody; pub mod add_gamenight_request_body; pub use self::add_gamenight_request_body::AddGamenightRequestBody; pub mod failure; pub use self::failure::Failure; +pub mod game; +pub use self::game::Game; +pub mod game_id; +pub use self::game_id::GameId; pub mod gamenight; pub use self::gamenight::Gamenight; pub mod gamenight_id; diff --git a/gamenight-cli/src/flows/add_game.rs b/gamenight-cli/src/flows/add_game.rs new file mode 100644 index 0000000..2a9c8bb --- /dev/null +++ b/gamenight-cli/src/flows/add_game.rs @@ -0,0 +1,44 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use gamenight_api_client_rs::{apis::default_api::game_post, models::{AddGameRequestBody, Game}}; +use inquire::{Confirm, Text}; + + +use super::*; + + +#[derive(Clone)] +pub struct AddGame { +} + +impl AddGame { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl<'a> Flow<'a> for AddGame { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + if let Some(name) = Text::new("What is the game you want to add").prompt_skippable()? { + let add_game_request = AddGameRequestBody { + name + }; + game_post(&state.api_configuration, Some(add_game_request)).await?; + + if let Some(owned) = Confirm::new("Do you own this game?").prompt_skippable()? { + if owned { + todo!() + } + } + } + Ok((FlowOutcome::Cancelled, state)) + } +} + +impl Display for AddGame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Add Game") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/edit_game.rs b/gamenight-cli/src/flows/edit_game.rs new file mode 100644 index 0000000..31d6fd0 --- /dev/null +++ b/gamenight-cli/src/flows/edit_game.rs @@ -0,0 +1,31 @@ + +use gamenight_api_client_rs::models::Game; + +use super::*; + +#[derive(Clone)] +pub struct EditGame { + game: Game +} + +impl EditGame { + pub fn new(game: Game) -> Self { + Self { + game + } + } +} + +#[async_trait] +impl<'a> Flow<'a> for EditGame { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + todo!() + } +} + +impl Display for EditGame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + diff --git a/gamenight-cli/src/flows/gamenight_menu.rs b/gamenight-cli/src/flows/gamenight_menu.rs index 3d952af..8363d3c 100644 --- a/gamenight-cli/src/flows/gamenight_menu.rs +++ b/gamenight-cli/src/flows/gamenight_menu.rs @@ -1,7 +1,7 @@ use inquire::{ui::RenderConfig, Select}; -use crate::flows::{add_gamenight::AddGamenight, exit::Exit, list_gamenights::ListGamenights}; +use crate::flows::{add_gamenight::AddGamenight, exit::Exit, games::Games, list_gamenights::ListGamenights}; use super::*; @@ -26,6 +26,7 @@ impl<'a> Flow<'a> for GamenightMenu { let flows: Vec> = vec![ Box::new(ListGamenights::new()), Box::new(AddGamenight::new()), + Box::new(Games::new()), Box::new(Exit::new()) ]; diff --git a/gamenight-cli/src/flows/games.rs b/gamenight-cli/src/flows/games.rs new file mode 100644 index 0000000..85e2597 --- /dev/null +++ b/gamenight-cli/src/flows/games.rs @@ -0,0 +1,47 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use inquire::{ui::RenderConfig, Select}; + +use crate::flows::{add_game::AddGame, exit::Exit, list_games::ListGames}; + +use super::*; + + +#[derive(Clone)] +pub struct Games { +} + +impl Games { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl<'a> Flow<'a> for Games { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + let flows: Vec> = vec![ + Box::new(ListGames::new()), + Box::new(AddGame::new()), + Box::new(Exit::new()) + ]; + + let choice = Select::new("What would you like to do?", flows) + .with_help_message("Select the action you want to take or quit the program") + .with_render_config(RenderConfig { + option_index_prefix: inquire::ui::IndexPrefix::Simple, + ..Default::default() + }) + .prompt_skippable()?; + + clear_screen::clear(); + handle_choice_option(&choice, self, state).await + } +} + +impl Display for Games { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Games") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/list_games.rs b/gamenight-cli/src/flows/list_games.rs new file mode 100644 index 0000000..a0481cd --- /dev/null +++ b/gamenight-cli/src/flows/list_games.rs @@ -0,0 +1,51 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use gamenight_api_client_rs::apis::default_api::games_get; +use inquire::{ui::RenderConfig, Select}; + +use crate::flows::{edit_game::EditGame, exit::Exit}; + +use super::*; + + +#[derive(Clone)] +pub struct ListGames { +} + +impl ListGames { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl<'a> Flow<'a> for ListGames { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + let games = games_get(&state.api_configuration).await?; + + let mut flows = games.into_iter().map(|game| -> Box { + Box::new(EditGame::new(game)) + }).collect::>>(); + + flows.push(Box::new(Exit::new())); + + let choice = Select::new("What would you like to do?", flows) + .with_help_message("Select the action you want to take or quit the program") + .with_render_config(RenderConfig { + option_index_prefix: inquire::ui::IndexPrefix::Simple, + ..Default::default() + }) + .prompt_skippable()?; + + clear_screen::clear(); + handle_choice_option(&choice, self, state).await + + } +} + +impl Display for ListGames { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "List all games") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/mod.rs b/gamenight-cli/src/flows/mod.rs index ce55a66..0dab210 100644 --- a/gamenight-cli/src/flows/mod.rs +++ b/gamenight-cli/src/flows/mod.rs @@ -20,6 +20,10 @@ mod join; mod leave; mod connect; mod settings; +mod games; +mod list_games; +mod add_game; +mod edit_game; pub struct GamenightState { api_configuration: Configuration, diff --git a/gamenight-database/diesel.toml b/gamenight-database/diesel.toml index ae76b11..f57985a 100644 --- a/gamenight-database/diesel.toml +++ b/gamenight-database/diesel.toml @@ -1,2 +1,2 @@ [print_schema] -file = "src/schema/schema.rs" +file = "src/schema.rs" diff --git a/gamenight-database/migrations/2022-03-19-191822_initial/down.sql b/gamenight-database/migrations/2022-03-19-191822_initial/down.sql index be43968..e8b3dd5 100644 --- a/gamenight-database/migrations/2022-03-19-191822_initial/down.sql +++ b/gamenight-database/migrations/2022-03-19-191822_initial/down.sql @@ -1,4 +1,4 @@ -- This file should undo anything in `up.sql` drop table gamenight; -drop table known_games; \ No newline at end of file +drop table game; \ No newline at end of file diff --git a/gamenight-database/migrations/2022-03-19-191822_initial/up.sql b/gamenight-database/migrations/2022-03-19-191822_initial/up.sql index 3513943..0fe17d6 100644 --- a/gamenight-database/migrations/2022-03-19-191822_initial/up.sql +++ b/gamenight-database/migrations/2022-03-19-191822_initial/up.sql @@ -6,7 +6,7 @@ CREATE TABLE gamenight ( datetime VARCHAR NOT NULL ); -CREATE TABLE known_games ( +CREATE TABLE game ( id UUID NOT NULL PRIMARY KEY, name VARCHAR UNIQUE NOT NULL ); \ No newline at end of file diff --git a/gamenight-database/migrations/2022-04-17-175115_user-system/down.sql b/gamenight-database/migrations/2022-04-17-175115_user-system/down.sql index 0f009fb..b66e08d 100644 --- a/gamenight-database/migrations/2022-04-17-175115_user-system/down.sql +++ b/gamenight-database/migrations/2022-04-17-175115_user-system/down.sql @@ -1,6 +1,6 @@ -- This file should undo anything in `up.sql` drop table pwd; -drop table users; +drop table client; drop type Role; \ No newline at end of file diff --git a/gamenight-database/migrations/2022-04-17-175115_user-system/up.sql b/gamenight-database/migrations/2022-04-17-175115_user-system/up.sql index da862fb..1425ea8 100644 --- a/gamenight-database/migrations/2022-04-17-175115_user-system/up.sql +++ b/gamenight-database/migrations/2022-04-17-175115_user-system/up.sql @@ -1,6 +1,6 @@ CREATE TYPE Role AS ENUM ('user', 'admin'); -CREATE TABLE users ( +CREATE TABLE client ( id UUID NOT NULL PRIMARY KEY, username VARCHAR UNIQUE NOT NULL, email VARCHAR UNIQUE NOT NULL, @@ -10,7 +10,7 @@ CREATE TABLE users ( CREATE TABLE pwd ( user_id UUID NOT NULL PRIMARY KEY, password VARCHAR NOT NULL, - CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES client(id) ON DELETE CASCADE ); --Initialize default admin user, with password "gamenight!" @@ -19,7 +19,7 @@ DO $$ DECLARE admin_uuid uuid = uuid_generate_v4(); BEGIN - INSERT INTO users (id, username, email, role) + INSERT INTO client (id, username, email, role) values(admin_uuid, 'admin', '', 'admin'); insert INTO pwd (user_id, password) diff --git a/gamenight-database/migrations/2022-05-14-104118_gamenight_owners/up.sql b/gamenight-database/migrations/2022-05-14-104118_gamenight_owners/up.sql index 76b28ff..2130f62 100644 --- a/gamenight-database/migrations/2022-05-14-104118_gamenight_owners/up.sql +++ b/gamenight-database/migrations/2022-05-14-104118_gamenight_owners/up.sql @@ -5,7 +5,7 @@ CREATE TABLE gamenight ( name VARCHAR NOT NULL, datetime VARCHAR NOT NULL, owner_id UUID NOT NULL, - CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES client(id) ON DELETE CASCADE ); SET session_replication_role = 'replica'; diff --git a/gamenight-database/migrations/2022-05-27-183310_gamenight_gamelist/up.sql b/gamenight-database/migrations/2022-05-27-183310_gamenight_gamelist/up.sql index 153a8f4..75066a7 100644 --- a/gamenight-database/migrations/2022-05-27-183310_gamenight_gamelist/up.sql +++ b/gamenight-database/migrations/2022-05-27-183310_gamenight_gamelist/up.sql @@ -4,6 +4,6 @@ create table gamenight_gamelist ( gamenight_id UUID NOT NULL, game_id UUID NOT NULL, CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE, - CONSTRAINT FK_game_id FOREIGN KEY (game_id) REFERENCES known_games(id) ON DELETE CASCADE, + CONSTRAINT FK_game_id FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE, PRIMARY KEY(gamenight_id, game_id) ); \ No newline at end of file diff --git a/gamenight-database/migrations/2022-05-28-142526_gamenight participant/up.sql b/gamenight-database/migrations/2022-05-28-142526_gamenight participant/up.sql index 51fd33a..ff8bd8b 100644 --- a/gamenight-database/migrations/2022-05-28-142526_gamenight participant/up.sql +++ b/gamenight-database/migrations/2022-05-28-142526_gamenight participant/up.sql @@ -4,6 +4,6 @@ create table gamenight_participant ( gamenight_id UUID NOT NULL, user_id UUID NOT NULL, CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE, - CONSTRAINT FK_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT FK_user_id FOREIGN KEY (user_id) REFERENCES client(id) ON DELETE CASCADE, PRIMARY KEY(gamenight_id, user_id) ) \ No newline at end of file diff --git a/gamenight-database/migrations/2025-06-27-171106_owned_games/down.sql b/gamenight-database/migrations/2025-06-27-171106_owned_games/down.sql new file mode 100644 index 0000000..d63e8d7 --- /dev/null +++ b/gamenight-database/migrations/2025-06-27-171106_owned_games/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +drop table owned_game; \ No newline at end of file diff --git a/gamenight-database/migrations/2025-06-27-171106_owned_games/up.sql b/gamenight-database/migrations/2025-06-27-171106_owned_games/up.sql new file mode 100644 index 0000000..3e952d1 --- /dev/null +++ b/gamenight-database/migrations/2025-06-27-171106_owned_games/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +create table owned_game ( + user_id UUID NOT NULL, + game_id UUID NOT NULL, + CONSTRAINT FK_gamenight_id FOREIGN KEY (user_id) REFERENCES client(id) ON DELETE CASCADE, + CONSTRAINT FK_user_id FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE, + PRIMARY KEY(user_id, game_id) +) \ No newline at end of file diff --git a/gamenight-database/src/game.rs b/gamenight-database/src/game.rs new file mode 100644 index 0000000..0199857 --- /dev/null +++ b/gamenight-database/src/game.rs @@ -0,0 +1,21 @@ +use diesel::{dsl::insert_into, Insertable, PgConnection, Queryable, RunQueryDsl}; +use serde::{Serialize, Deserialize}; +use uuid::Uuid; +use crate::schema::game; + +use super::error::DatabaseError; + +#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] +#[diesel(table_name = game)] +pub struct Game { + pub id: Uuid, + pub name: String, +} + +pub fn games(conn: &mut PgConnection) -> Result, DatabaseError> { + Ok(game::table.load::(conn)?) +} + +pub fn insert_game(conn: &mut PgConnection, game: Game) -> Result { + Ok(insert_into(game::table).values(&game).execute(conn)?) +} \ No newline at end of file diff --git a/gamenight-database/src/lib.rs b/gamenight-database/src/lib.rs index 7fc77da..488fc91 100644 --- a/gamenight-database/src/lib.rs +++ b/gamenight-database/src/lib.rs @@ -3,6 +3,7 @@ pub mod error; pub mod schema; pub mod gamenight; pub mod gamenight_participants; +pub mod game; use diesel::r2d2::ConnectionManager; use diesel::r2d2::ManageConnection; @@ -17,6 +18,7 @@ pub use user::login; pub use user::register; pub use gamenight::gamenights; pub use gamenight_participants::get_participants; +pub use game::games; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub type DbConnection = PgConnection; diff --git a/gamenight-database/src/schema.rs b/gamenight-database/src/schema.rs index cd1f2af..0056e00 100644 --- a/gamenight-database/src/schema.rs +++ b/gamenight-database/src/schema.rs @@ -6,6 +6,25 @@ pub mod sql_types { pub struct Role; } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::Role; + + client (id) { + id -> Uuid, + username -> Varchar, + email -> Varchar, + role -> Role, + } +} + +diesel::table! { + game (id) { + id -> Uuid, + name -> Varchar, + } +} + diesel::table! { gamenight (id) { id -> Uuid, @@ -30,9 +49,9 @@ diesel::table! { } diesel::table! { - known_games (id) { - id -> Uuid, - name -> Varchar, + owned_game (user_id, game_id) { + user_id -> Uuid, + game_id -> Uuid, } } @@ -53,31 +72,22 @@ diesel::table! { } } -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::Role; - - users (id) { - id -> Uuid, - username -> Varchar, - email -> Varchar, - role -> Role, - } -} - -diesel::joinable!(gamenight -> users (owner_id)); +diesel::joinable!(gamenight -> client (owner_id)); +diesel::joinable!(gamenight_gamelist -> game (game_id)); diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id)); -diesel::joinable!(gamenight_gamelist -> known_games (game_id)); +diesel::joinable!(gamenight_participant -> client (user_id)); diesel::joinable!(gamenight_participant -> gamenight (gamenight_id)); -diesel::joinable!(gamenight_participant -> users (user_id)); -diesel::joinable!(pwd -> users (user_id)); +diesel::joinable!(owned_game -> client (user_id)); +diesel::joinable!(owned_game -> game (game_id)); +diesel::joinable!(pwd -> client (user_id)); diesel::allow_tables_to_appear_in_same_query!( + client, + game, gamenight, gamenight_gamelist, gamenight_participant, - known_games, + owned_game, pwd, registration_tokens, - users, ); diff --git a/gamenight-database/src/schema/schema.rs b/gamenight-database/src/schema/schema.rs deleted file mode 100644 index cd1f2af..0000000 --- a/gamenight-database/src/schema/schema.rs +++ /dev/null @@ -1,83 +0,0 @@ -// @generated automatically by Diesel CLI. - -pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "role"))] - pub struct Role; -} - -diesel::table! { - gamenight (id) { - id -> Uuid, - name -> Varchar, - datetime -> Timestamptz, - owner_id -> Uuid, - } -} - -diesel::table! { - gamenight_gamelist (gamenight_id, game_id) { - gamenight_id -> Uuid, - game_id -> Uuid, - } -} - -diesel::table! { - gamenight_participant (gamenight_id, user_id) { - gamenight_id -> Uuid, - user_id -> Uuid, - } -} - -diesel::table! { - known_games (id) { - id -> Uuid, - name -> Varchar, - } -} - -diesel::table! { - pwd (user_id) { - user_id -> Uuid, - password -> Varchar, - } -} - -diesel::table! { - registration_tokens (id) { - id -> Uuid, - #[max_length = 32] - token -> Bpchar, - single_use -> Bool, - expires -> Nullable, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::Role; - - users (id) { - id -> Uuid, - username -> Varchar, - email -> Varchar, - role -> Role, - } -} - -diesel::joinable!(gamenight -> users (owner_id)); -diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id)); -diesel::joinable!(gamenight_gamelist -> known_games (game_id)); -diesel::joinable!(gamenight_participant -> gamenight (gamenight_id)); -diesel::joinable!(gamenight_participant -> users (user_id)); -diesel::joinable!(pwd -> users (user_id)); - -diesel::allow_tables_to_appear_in_same_query!( - gamenight, - gamenight_gamelist, - gamenight_participant, - known_games, - pwd, - registration_tokens, - users, -); diff --git a/gamenight-database/src/user.rs b/gamenight-database/src/user.rs index 3e0f65e..a4681f5 100644 --- a/gamenight-database/src/user.rs +++ b/gamenight-database/src/user.rs @@ -13,7 +13,7 @@ use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::rand_core::RngCore; use crate::DbConnection; -use super::schema::{pwd, users}; +use super::schema::{pwd, client}; pub use super::error::DatabaseError; @@ -26,7 +26,7 @@ struct Pwd { } #[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] -#[diesel(table_name = users)] +#[diesel(table_name = client)] pub struct User { pub id: Uuid, pub username: String, @@ -65,10 +65,10 @@ pub struct Register { } pub fn login(conn: &mut DbConnection, user: LoginUser) -> Result, DatabaseError> { - let id: Uuid = users::table - .filter(users::username.eq(&user.username)) - .or_filter(users::email.eq(&user.username)) - .select(users::id) + let id: Uuid = client::table + .filter(client::username.eq(&user.username)) + .or_filter(client::email.eq(&user.username)) + .select(client::id) .first(conn)?; let pwd: String = pwd::table @@ -82,14 +82,14 @@ pub fn login(conn: &mut DbConnection, user: LoginUser) -> Result, D .verify_password(user.password.as_bytes(), &parsed_hash) .is_ok() { - Ok(Some(users::table.find(id).first(conn)?)) + Ok(Some(client::table.find(id).first(conn)?)) } else { Ok(None) } } pub fn get_user(conn: &mut DbConnection, id: Uuid) -> Result { - Ok(users::table.find(id).first(conn)?) + Ok(client::table.find(id).first(conn)?) } pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), DatabaseError> { @@ -105,7 +105,7 @@ pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), Datab conn.transaction(|c| { let id = Uuid::new_v4(); - diesel::insert_into(users::table) + diesel::insert_into(client::table) .values(User { id, username: register.username, @@ -126,15 +126,15 @@ pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), Datab } pub fn count_users_with_username(conn: &mut DbConnection, username: &String) -> Result { - Ok(users::table + Ok(client::table .count() - .filter(users::username.eq(username)) + .filter(client::username.eq(username)) .get_result::(conn)?) } pub fn count_users_with_email(conn: &mut DbConnection, email: &String) -> Result { - Ok(users::table + Ok(client::table .count() - .filter(users::email.eq(email)) + .filter(client::email.eq(email)) .get_result::(conn)?) } \ No newline at end of file