diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6ec7f02..5fe63a0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install latest nightly + - name: Install latest rust stable uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: stable override: true profile: minimal - name: Build @@ -44,10 +44,10 @@ jobs: steps: - name: Checkout latest commit uses: actions/checkout@v3 - - name: Install latest rust nightly + - name: Install latest rust stable uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: stable override: true profile: minimal - name: Diesel Migrations diff --git a/Cargo.lock b/Cargo.lock index 3de0fa0..402a90c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,8 +448,8 @@ dependencies = [ [[package]] name = "gamemstr-common" -version = "0.1.0" -source = "git+https://github.com/gamemstr/gamemstr-common.git?rev=d455255#d4552554494944320b6a6c6812d2be01825624e4" +version = "0.2.0-alpha.2" +source = "git+https://github.com/gamemstr/gamemstr-common.git?rev=c5a5615d3e980d810d92d60d85905335f8269fb3#c5a5615d3e980d810d92d60d85905335f8269fb3" dependencies = [ "num-to-words", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8ed2523..7b45ebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0" custom_derive = "0.1.7" diesel = { version = "2.0.3", features = ["postgres", "postgres_backend", "r2d2", "serde_json"] } dotenvy = "0.15.6" -gamemstr-common = { git = "https://github.com/gamemstr/gamemstr-common.git", rev="d455255"} +gamemstr-common = { git = "https://github.com/gamemstr/gamemstr-common.git", rev="c5a5615d3e980d810d92d60d85905335f8269fb3"} r2d2 = "0.8.10" r2d2-diesel = "1.0.0" rocket = { version = "0.5.0-rc.3", features = ["json"] } diff --git a/migrations/2024-02-08-142012_sessions/down.sql b/migrations/2024-02-08-142012_sessions/down.sql new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/migrations/2024-02-08-142012_sessions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/migrations/2024-02-08-142012_sessions/up.sql b/migrations/2024-02-08-142012_sessions/up.sql new file mode 100644 index 0000000..b9c5c7a --- /dev/null +++ b/migrations/2024-02-08-142012_sessions/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE sessions ( + id VARCHAR PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR NOT NULL, + campaign_id VARCHAR NOT NULL, + notes JSONB NOT NULL, + plan JSONB NOT NULL, + recap JSONB NOT NULL +); diff --git a/src/main.rs b/src/main.rs index 3f1fb0c..38badfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(debug_assertions, allow(unused_imports))] pub mod models; pub mod schema; mod services; @@ -32,6 +33,11 @@ fn rocket() -> _ { .mount("/", routes![services::worlds::locations::delete_location]) .mount("/", routes![services::worlds::locations::create_location]) .mount("/", routes![services::worlds::locations::update_location]) + .mount("/", routes![services::worlds::campaigns::sessions::list_sessions]) + .mount("/", routes![services::worlds::campaigns::sessions::get_session]) + .mount("/", routes![services::worlds::campaigns::sessions::delete_session]) + .mount("/", routes![services::worlds::campaigns::sessions::create_session]) + .mount("/", routes![services::worlds::campaigns::sessions::update_session]) .attach(Template::fairing()) } @@ -395,4 +401,70 @@ mod tests { assert_eq!(response.status(), Status::Ok); client.delete("/worlds/2587598027928569265").dispatch(); } + + #[test] + #[should_panic] + pub fn test_sessions_api() { + let client = Client::tracked(crate::rocket()).expect("valid rocket instance"); + let world = gamemstr_common::world::World { + id: "2587598027928569265".to_string(), + name: "Test World".to_string(), + description: "Test Description".to_string(), + }; + client + .post("/worlds/add") + .header(ContentType::JSON) + .body(serde_json::to_string(&world).unwrap()) + .dispatch(); + // TODO: Add campaign + let response = client + .get("/worlds/2587598027928569265/campaigns/2587598027928569265/sessions") + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response.into_string().unwrap().contains("Sessions")); + let session = gamemstr_common::world::campaign::session::Session { + id: "2587598027928569265".to_string(), + name: "Test World".to_string(), + description: "Test Description".to_string(), + campaign_id: "2587598027928569265".to_string(), + notes: vec![], + plan: gamemstr_common::world::campaign::session::Plan::default(), + recap: gamemstr_common::world::campaign::session::Recap::default(), + }; + client + .post("/worlds/2587598027928569265/campaigns/2587598027928569265/sessions/add") + .header(ContentType::JSON) + .body(serde_json::to_string(&session).unwrap()) + .dispatch(); + let response = client + .get("/worlds/2587598027928569265/campaigns/2587598027928569265/sessions/2587598027928569265") + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!( + response.into_string(), + Some(serde_json::to_string(&session).unwrap()) + ); + let new_session = gamemstr_common::world::campaign::session::Session { + name: "Updated Session".to_string(), + ..session + }; + let response = client + .post("/worlds/2587598027928569265/campaigns/2587598027928569265/sessions/2587598027928569265") + .header(ContentType::JSON) + .body(serde_json::to_string(&new_session).unwrap()) + .dispatch(); + assert_eq!(response.status(), Status::Accepted); + assert_eq!( + serde_json::from_str::( + &response.into_string().unwrap() + ) + .unwrap() + .name, + "Updated Session" + ); + let response = client + .delete("/worlds/2587598027928569265/campaigns/2587598027928569265/sessions/2587598027928569265") + .dispatch(); + assert_eq!(response.status(), Status::Ok); + } } diff --git a/src/models/worlds.rs b/src/models/worlds.rs index f883126..80a959a 100644 --- a/src/models/worlds.rs +++ b/src/models/worlds.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::schema::worlds; +pub mod campaigns; pub mod locations; #[derive(Debug, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] diff --git a/src/models/worlds/campaigns.rs b/src/models/worlds/campaigns.rs new file mode 100644 index 0000000..6824cc9 --- /dev/null +++ b/src/models/worlds/campaigns.rs @@ -0,0 +1 @@ +pub mod sessions; \ No newline at end of file diff --git a/src/models/worlds/campaigns/sessions.rs b/src/models/worlds/campaigns/sessions.rs new file mode 100644 index 0000000..5cfb3c5 --- /dev/null +++ b/src/models/worlds/campaigns/sessions.rs @@ -0,0 +1,44 @@ +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::schema::sessions; + +#[derive(Debug, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] +#[diesel(table_name = sessions)] +pub struct Session { + pub id: String, + pub name: String, + pub campaign_id: String, + pub description: String, + pub notes: serde_json::Value, + pub plan: serde_json::Value, + pub recap: serde_json::Value, +} + +impl super::super::super::Model for Session { + type Entity = gamemstr_common::world::campaign::session::Session; + + fn new(entity: Self::Entity) -> Self { + Self { + id: entity.id, + name: entity.name, + campaign_id: entity.campaign_id, + description: entity.description, + notes: serde_json::to_value(entity.notes).unwrap(), + plan: serde_json::to_value(entity.plan).unwrap(), + recap: serde_json::to_value(entity.recap).unwrap(), + } + } + + fn to_entity(&self) -> Self::Entity { + Self::Entity { + id: self.id.clone(), + name: self.name.clone(), + campaign_id: self.campaign_id.clone(), + description: self.description.clone(), + notes: serde_json::from_value(self.notes.clone()).unwrap(), + plan: serde_json::from_value(self.plan.clone()).unwrap(), + recap: serde_json::from_value(self.recap.clone()).unwrap(), + } + } +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index 9706af9..4825df9 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -56,6 +56,18 @@ diesel::table! { } } +diesel::table! { + sessions (id) { + id -> Varchar, + name -> Varchar, + description -> Varchar, + campaign_id -> Varchar, + notes -> Jsonb, + plan -> Jsonb, + recap -> Jsonb, + } +} + diesel::table! { spells (id) { id -> Varchar, @@ -82,4 +94,11 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!(creatures, items, locations, spells, worlds,); +diesel::allow_tables_to_appear_in_same_query!( + creatures, + items, + locations, + sessions, + spells, + worlds, +); diff --git a/src/services/worlds.rs b/src/services/worlds.rs index 0881790..63eac78 100644 --- a/src/services/worlds.rs +++ b/src/services/worlds.rs @@ -13,6 +13,7 @@ use rocket::{ }; use rocket_dyn_templates::{context, Template}; +pub mod campaigns; pub mod locations; #[get("/worlds")] diff --git a/src/services/worlds/campaigns.rs b/src/services/worlds/campaigns.rs new file mode 100644 index 0000000..6824cc9 --- /dev/null +++ b/src/services/worlds/campaigns.rs @@ -0,0 +1 @@ +pub mod sessions; \ No newline at end of file diff --git a/src/services/worlds/campaigns/sessions.rs b/src/services/worlds/campaigns/sessions.rs new file mode 100644 index 0000000..9992472 --- /dev/null +++ b/src/services/worlds/campaigns/sessions.rs @@ -0,0 +1,101 @@ +use super::super::super::Result; +use crate::{ + models::{self, Model}, + schema, +}; +use diesel::prelude::*; +use gamemstr_common::world::campaign::session::Session; +use rocket::{ + delete, get, post, + response::status::{Accepted, Created, NotFound}, + serde::json::Json, + Either, +}; +use rocket_dyn_templates::{context, Template}; + +#[get("/worlds/<_world_id>/campaigns//sessions")] +pub fn list_sessions(_world_id: String, campaign_id: String) -> Template { + let connection = &mut crate::services::establish_connection_pg(); + let sessions = schema::sessions::dsl::sessions + .filter(schema::sessions::dsl::campaign_id.eq(campaign_id)) + .load::(connection) + .expect("Error loading sessions") + .iter() + .map(|session| session.to_entity()) + .collect::>(); + Template::render( + "sessions", + context! { + sessions: &sessions, + count: sessions.len() + }, + ) +} + +#[get("/worlds/<_world_id>/campaigns/<_campaign_id>/sessions/")] +pub fn get_session(_world_id: String, _campaign_id: String, id: String) -> Result> { + let connection = &mut crate::services::establish_connection_pg(); + let session = schema::sessions::dsl::sessions + .find(id) + .first::(connection) + .expect("Error loading session") + .to_entity(); + Ok(Json(session)) +} + +#[delete("/worlds/<_world_id>/campaigns/<_campaign_id>/sessions/")] +pub fn delete_session(_world_id: String, _campaign_id: String, id: String) -> Result> { + let connection = &mut crate::services::establish_connection_pg(); + let session = schema::sessions::dsl::sessions + .find(&id) + .first::(connection) + .expect("Error loading session") + .to_entity(); + diesel::delete(schema::sessions::dsl::sessions.find(&id)) + .execute(connection) + .expect("Error deleting session"); + Ok(Json(session)) +} + +#[post( + "/worlds/<_world_id>/campaigns/<_campaign_id>/sessions/add", + rank = 1, + format = "json", + data = "" +)] +pub fn create_session( + _world_id: String, + _campaign_id: String, + session: Json, +) -> Result>> { + let connection = &mut crate::services::establish_connection_pg(); + let new_session = models::worlds::campaigns::sessions::Session::new(session.clone().0); + diesel::insert_into(schema::sessions::dsl::sessions) + .values(&new_session) + .execute(connection) + .expect("Error creating session"); + Ok(Created::new("/").body(session)) +} + +#[post( + "/worlds/<_world_id>/campaigns/<_campaign_id>/sessions/", + rank = 2, + format = "json", + data = "" +)] +pub fn update_session( + _world_id: String, + _campaign_id: String, + id: String, + session: Json, +) -> Either>>, Result>> { + let connection = &mut crate::services::establish_connection_pg(); + let new_session = models::worlds::campaigns::sessions::Session::new(session.clone().0); + let result = diesel::update(schema::sessions::dsl::sessions.find(&id)) + .set(&new_session) + .execute(connection); + match result { + Ok(_) => Either::Left(Ok(Accepted(Some(session)))), + Err(_) => Either::Right(Ok(NotFound(format!("Session not found with id {}", id)))), + } +} diff --git a/templates/sessions.html.hbs b/templates/sessions.html.hbs new file mode 100644 index 0000000..562f1eb --- /dev/null +++ b/templates/sessions.html.hbs @@ -0,0 +1,25 @@ + + + + + + + Sessions + + +
+

Sessions

+ {{#each sessions as |session|}} +
    +
  • ID: {{session.id}}
  • +
  • Name: {{session.name}}
  • +
  • Description: {{session.description}}
  • +
  • Campaign ID: {{session.campaign_id}}
  • +
  • Notes: {{session.notes}}
  • +
  • Plan: {{session.plan}}
  • +
  • Recap: {{session.recap}}
  • +
+ {{/each}} +
+ + \ No newline at end of file