Spells endpoints

This commit is contained in:
Benjamin Sherriff
2023-10-03 14:21:53 -04:00
parent 95ede3291e
commit 16d8fa5af8
10 changed files with 330 additions and 98 deletions

View File

@@ -1,8 +1,13 @@
RUST_LOG=warn,siren=info RUST_LOG=warn,siren=info
DATABASE_USER=siren DATABASE_USER=siren
DATABASE_PASSWORD= DATABASE_PASSWORD=
DATABASE_NAME=siren DATABASE_NAME=siren
DATABASE_HOST=localhost DATABASE_HOST=localhost
DATABASE_PORT=5432 DATABASE_PORT=5432
SERVICE_HOST=localhost
SERVICE_PORT=5000
DISCORD_TOKEN= DISCORD_TOKEN=
OPENAI_API_KEY= OPENAI_API_KEY=

View File

@@ -14,6 +14,8 @@ services:
environment: environment:
DATABASE_HOST: db DATABASE_HOST: db
DATABASE_PORT: 5432 DATABASE_PORT: 5432
SERVICE_HOST: siren
SERVICE_PORT: 5000
depends_on: depends_on:
- db - db
restart: unless-stopped restart: unless-stopped

View File

@@ -2,17 +2,17 @@ CREATE TABLE IF NOT EXISTS spells (
id INTEGER GENERATED ALWAYS AS IDENTITY, id INTEGER GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL, name TEXT NOT NULL,
school TEXT NOT NULL, school TEXT NOT NULL,
level TEXT NOT NULL, level INTEGER NOT NULL,
ritual BOOLEAN DEFAULT FALSE, ritual BOOLEAN DEFAULT FALSE,
casting_time TEXT NOT NULL, casting_time TEXT NOT NULL,
range TEXT NOT NULL, range TEXT NOT NULL,
components_verbal BOOLEAN DEFAULT FALSE, components_verbal BOOLEAN DEFAULT FALSE,
components_somatic BOOLEAN DEFAULT FALSE, components_somatic BOOLEAN DEFAULT FALSE,
components_material BOOLEAN DEFAULT FALSE, components_material BOOLEAN DEFAULT FALSE,
components_materials_needed TEXT components_materials_needed TEXT,
duration TEXT NOT NULL, duration TEXT NOT NULL,
classes TEXT[] NOT NULL, classes TEXT[] NOT NULL,
sources TEXT[] NOT NULL, sources TEXT[] NOT NULL,
tags TEXT[] tags TEXT[],
description TEXT NOT NULL description TEXT NOT NULL
); );

View File

@@ -12,7 +12,7 @@ use serenity::model::channel::Message;
use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverwriteType}; use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverwriteType};
use serenity::prelude::*; use serenity::prelude::*;
use crate::db::{connection, NewMessageDB, MessageDB}; use crate::db::{connection, messages::{MessageDB, NewMessageDB}};
pub struct OAI { pub struct OAI {
pub client: reqwest::Client, pub client: reqwest::Client,

View File

@@ -1,26 +1,25 @@
use crate::error_handler::ServiceError; use crate::error_handler::ServiceError;
use diesel::{r2d2::ConnectionManager, PgConnection}; use diesel::{r2d2::ConnectionManager, PgConnection};
use serde::{Deserialize, Serialize};
use crate::diesel_migrations::MigrationHarness; use crate::diesel_migrations::MigrationHarness;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{error, info}; use log::{error, info};
use r2d2; use r2d2;
use std::env; use std::env;
mod backgrounds; pub mod backgrounds;
mod bestiary; pub mod bestiary;
mod classes; pub mod classes;
mod conditions; pub mod conditions;
mod feats; pub mod feats;
mod items; pub mod items;
mod messages; pub mod messages;
mod options; pub mod options;
mod races; pub mod races;
mod spells; pub mod spells;
mod users; pub mod users;
pub mod schema; pub mod schema;
pub use messages::*;
type Pool = r2d2::Pool<ConnectionManager<PgConnection>>; type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>; pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
@@ -52,3 +51,21 @@ pub fn connection() -> Result<DbConnection, ServiceError> {
POOL.get() POOL.get()
.map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e))) .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e)))
} }
pub fn load_data() {
spells::load_data();
}
#[derive(Serialize, Deserialize)]
pub struct GetResponse<T> {
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>
}
#[derive(Serialize, Deserialize)]
pub struct Metadata {
pub total: i32,
pub limit: i32,
pub page: i32
}

View File

@@ -1,3 +1,43 @@
mod model; mod model;
mod routes;
pub use model::*; pub use model::*;
pub use routes::init_routes;
pub fn load_data() {
let root_path = std::env::current_dir().unwrap();
let mut data_path = std::path::PathBuf::from(root_path);
data_path.push("data/spells.json");
match data_path.to_str() {
Some(path) => {
log::debug!("Loading spells from {}", path);
match std::fs::read_to_string(data_path) {
Ok(data) => {
match serde_json::from_str::<serde_json::Value>(&data) {
Ok(json) => {
match serde_json::from_value::<Vec<Spell>>(json) {
Ok(spells) => {
let count = QuerySpell::get_count().unwrap();
if count >= spells.len() as i64 {
log::warn!("Spell data is already loaded");
return;
}
for spell in spells {
match InsertSpell::insert(spell.into()) {
Ok(_) => {},
Err(err) => log::error!("Failed to insert spell: {}", err)
}
}
},
Err(err) => log::error!("Failed to parse spells data: {}", err)
}
},
Err(err) => log::error!("Failed to parse spells data to value: {}", err)
};
},
Err(err) => log::error!("Failed to read spells data: {}", err)
};
},
None => log::error!("Failed to find spells data directory")
}
}

View File

@@ -1,7 +1,7 @@
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::schema::spells; use crate::{db::schema::spells, error_handler::ServiceError};
#[derive(Queryable, QueryableByName)] #[derive(Queryable, QueryableByName)]
#[diesel(table_name = spells)] #[diesel(table_name = spells)]
@@ -24,6 +24,38 @@ pub struct QuerySpell {
pub description: String pub description: String
} }
impl QuerySpell {
pub fn get_all(limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
let mut conn = crate::db::connection()?;
let mut query = spells::table
.limit(limit as i64)
.into_boxed();
query = query.filter(spells::id.gt(std::cmp::max(0, page - 1) * limit));
let spells = query.load::<QuerySpell>(&mut conn)?;
Ok(spells)
}
pub fn get_by_id(id: i32) -> Result<Self, ServiceError> {
let mut conn = crate::db::connection()?;
let spell = spells::table
.filter(spells::id.eq(id))
.first::<QuerySpell>(&mut conn)?;
Ok(spell)
}
pub fn get_count() -> Result<i64, ServiceError> {
let mut conn = crate::db::connection()?;
let count = spells::table.count().get_result(&mut conn)?;
Ok(count)
}
pub fn delete(id: i32) -> Result<Self, ServiceError> {
let mut conn = crate::db::connection()?;
let spell = diesel::delete(spells::table.filter(spells::id.eq(id))).get_result(&mut conn)?;
Ok(spell)
}
}
#[derive(Insertable, AsChangeset)] #[derive(Insertable, AsChangeset)]
#[diesel(table_name = spells)] #[diesel(table_name = spells)]
pub struct InsertSpell { pub struct InsertSpell {
@@ -44,6 +76,20 @@ pub struct InsertSpell {
pub description: String pub description: String
} }
impl InsertSpell {
pub fn insert(spell: Self) -> Result<QuerySpell, ServiceError> {
let mut conn = crate::db::connection()?;
let spell = diesel::insert_into(spells::table).values(spell).get_result(&mut conn)?;
Ok(spell)
}
pub fn update(id: i32, spell: Self) -> Result<QuerySpell, ServiceError> {
let mut conn = crate::db::connection()?;
let spell = diesel::update(spells::table.filter(spells::id.eq(id))).set(spell).get_result(&mut conn)?;
Ok(spell)
}
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Spell { pub struct Spell {
pub name: String, pub name: String,
@@ -56,6 +102,7 @@ pub struct Spell {
pub duration: String, pub duration: String,
pub classes: Vec<String>, pub classes: Vec<String>,
pub sources: Vec<String>, pub sources: Vec<String>,
pub tags: Vec<String>,
pub description: String pub description: String
} }
@@ -67,30 +114,8 @@ pub struct Components {
pub materials_needed: Option<String> pub materials_needed: Option<String>
} }
impl Spell { impl From<QuerySpell> for Spell {
/// Convert spell to insertable struct fn from(query: QuerySpell) -> Self {
pub fn to_insert(self) -> InsertSpell {
return InsertSpell {
name: self.name,
school: self.school,
level: self.level,
ritual: self.ritual,
casting_time: self.casting_time,
range: self.range,
components_verbal: self.components.verbal,
components_somatic: self.components.somatic,
components_material: self.components.material,
components_materials_needed: self.components.materials_needed,
duration: self.duration,
classes: self.classes,
sources: self.sources,
tags: vec![],
description: self.description
}
}
/// Convert query struct to spell
pub fn from_query(query: QuerySpell) -> Self {
return Self { return Self {
name: query.name, name: query.name,
school: query.school, school: query.school,
@@ -107,14 +132,30 @@ impl Spell {
duration: query.duration, duration: query.duration,
classes: query.classes, classes: query.classes,
sources: query.sources, sources: query.sources,
tags: query.tags,
description: query.description description: query.description
} }
} }
}
/// Convert file to spell impl Into<InsertSpell> for Spell {
pub fn from_file(file: String) -> Self { fn into(self) -> InsertSpell {
let data = std::fs::read_to_string(file).unwrap(); return InsertSpell {
let spell: Spell = serde_json::from_str(&data).unwrap(); name: self.name,
return spell; school: self.school,
level: self.level,
ritual: self.ritual,
casting_time: self.casting_time,
range: self.range,
components_verbal: self.components.verbal,
components_somatic: self.components.somatic,
components_material: self.components.material,
components_materials_needed: self.components.materials_needed,
duration: self.duration,
classes: self.classes,
sources: self.sources,
tags: self.tags,
description: self.description
}
} }
} }

79
src/db/spells/routes.rs Normal file
View File

@@ -0,0 +1,79 @@
use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError};
use serde::{Serialize, Deserialize};
use crate::db::{spells::QuerySpell, GetResponse, Metadata};
use super::{Spell, InsertSpell};
#[derive(Serialize, Deserialize)]
struct GetAllParams {
limit: Option<i32>,
page: Option<i32>,
}
#[get("/spells")]
async fn get_all(req: HttpRequest) -> HttpResponse {
let params = web::Query::<GetAllParams>::from_query(req.query_string()).unwrap();
let limit = params.limit.unwrap_or(20);
let page = params.page.unwrap_or(1);
match web::block(move || QuerySpell::get_all(limit, page)).await.unwrap() {
Ok(spells) => {
let mut response: Vec<Spell> = Vec::new();
for spell in spells {
response.push(Spell::from(spell));
}
let total_count = QuerySpell::get_count().unwrap();
HttpResponse::Ok().json(GetResponse {
data: response,
metadata: Some(Metadata {
total: total_count as i32,
limit,
page
})
})
},
Err(err) => ResponseError::error_response(&err)
}
}
#[get("/spells/{id}")]
async fn get_by_id(id: web::Path<i32>) -> HttpResponse {
match web::block(move || QuerySpell::get_by_id(id.into_inner())).await.unwrap() {
Ok(spell) => HttpResponse::Ok().json(GetResponse {
data: Spell::from(spell),
metadata: None
}),
Err(err) => ResponseError::error_response(&err)
}
}
#[post("/spells")]
async fn create(spell: web::Json<Spell>) -> HttpResponse {
match InsertSpell::insert(spell.into_inner().into()) {
Ok(spell) => HttpResponse::Created().json(Spell::from(spell)),
Err(err) => ResponseError::error_response(&err)
}
}
#[put("/spells/{id}")]
async fn update(id: web::Path<i32>, spell: web::Json<Spell>) -> HttpResponse {
match web::block(move || InsertSpell::update(id.into_inner(), spell.into_inner().into())).await.unwrap() {
Ok(spell) => HttpResponse::Ok().json(Spell::from(spell)),
Err(err) => ResponseError::error_response(&err)
}
}
#[delete("/spells/{id}")]
async fn delete(id: web::Path<i32>) -> HttpResponse {
match web::block(move || QuerySpell::delete(id.into_inner())).await.unwrap() {
Ok(spell) => HttpResponse::Ok().json(Spell::from(spell)),
Err(err) => ResponseError::error_response(&err)
}
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(get_all);
config.service(get_by_id);
config.service(create);
config.service(delete);
}

View File

@@ -1,4 +1,6 @@
use actix_web::{ResponseError, HttpResponse};
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
@@ -29,8 +31,27 @@ impl From<DieselError> for ServiceError {
DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()), DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()),
DieselError::NotFound => { DieselError::NotFound => {
ServiceError::new(404, "The record was not found".to_string()) ServiceError::new(404, "The record was not found".to_string())
} },
DieselError::SerializationError(err) => {
ServiceError::new(422, err.to_string())
},
err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)), err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)),
} }
} }
} }
impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse {
let status_code = match StatusCode::from_u16(self.error_status_code) {
Ok(status_code) => status_code,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let error_message = match status_code.as_u16() < 500 {
true => self.error_message.clone(),
false => "Internal server error".to_string(),
};
HttpResponse::build(status_code).json(serde_json::json!({ "message": error_message }))
}
}

View File

@@ -6,6 +6,7 @@ use std::collections::{HashSet, HashMap};
use std::env; use std::env;
use std::sync::Arc; use std::sync::Arc;
use actix_web::{HttpServer, App};
use commands::audio::{create_response, AudioConfig, AudioConfigs}; use commands::audio::{create_response, AudioConfig, AudioConfigs};
use dotenv::dotenv; use dotenv::dotenv;
@@ -111,11 +112,38 @@ impl EventHandler for Handler {
} }
} }
#[tokio::main] #[actix_web::main]
async fn main() { async fn main() -> std::io::Result<()> {
dotenv().ok(); dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info"));
db::init();
db::load_data();
// setup_discord_bot();
let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string());
let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());
match HttpServer::new(|| {
App::new()
.configure(db::spells::init_routes)
})
.bind(format!("{}:{}", host, port)) {
Ok(b) => {
info!("Binding server to {}:{}", host, port);
b
},
Err(err) => {
error!("Could not bind server: {}", err);
return Err(err);
}
}
.run()
.await
}
fn setup_discord_bot() {
tokio::spawn(async {
let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let intents: GatewayIntents = GatewayIntents::all(); let intents: GatewayIntents = GatewayIntents::all();
@@ -136,8 +164,6 @@ async fn main() {
Err(why) => panic!("Could not access application info: {:?}", why) Err(why) => panic!("Could not access application info: {:?}", why)
}; };
db::init();
let handler = match env::var("OPENAI_API_KEY") { let handler = match env::var("OPENAI_API_KEY") {
Ok(token) => { Ok(token) => {
info!("Loaded OpenAI token"); info!("Loaded OpenAI token");
@@ -167,4 +193,5 @@ async fn main() {
if let Err(why) = client.start_autosharded().await { if let Err(why) = client.start_autosharded().await {
error!("An error occurred while running the client: {:?}", why); error!("An error occurred while running the client: {:?}", why);
} }
});
} }