use hyper::{StatusCode, Response, Body}; use hyper::header::HeaderValue; use mysql_async::{Conn, Pool}; use mysql_async::error::Error; use mysql_async::prelude::{params, Queryable}; use serde_json::Value; use crate::db_types::{UBigInt, VarChar, Integer}; use crate::common; #[derive(Debug)] pub enum ChannelType { Voice, Text, Undefined } impl ChannelType { // These funcs are mainly here to help translation from mysql pub fn from_i32(x: i32) -> ChannelType { match x { 1 => ChannelType::Voice, 2 => ChannelType::Text, _ => ChannelType::Undefined } } pub fn as_i32(&self) -> i32 { match self { ChannelType::Voice => 1, ChannelType::Text => 2, ChannelType::Undefined => 3, } } } // Primary way of interpretting sql data on our channels table pub struct Channel { pub id: u64, pub name: String, pub description: String, pub kind: ChannelType } #[derive(Debug)] struct InsertableChannel { name: String, kind: ChannelType } impl Channel { /* * When our sql library queries things we generally get back tuples rather reasily * we can use this method to get something that makes more sense */ fn from_tup(tup: (UBigInt, VarChar, Option, Integer)) -> Channel { let desc = match tup.2 { Some(val) => val, None => "None".into() }; Channel { id: tup.0, name: tup.1, description: desc, kind: ChannelType::from_i32(tup.3) } } /* * When responding with some channel data to the client we use json * this itemizes a single struct as the following(without the pretty output) * { * "id": id, * "name": "", * "description": Option<"">, * "kind": kind * } */ fn as_json_str(&self) -> String { let mut base = String::from("{"); base.push_str(&format!("\"id\":{},", self.id)); base.push_str(&format!("\"name\":\"{}\",", self.name)); base.push_str(&format!("\"description\":\"{}\",", self.description)); base.push_str(&format!("\"kind\":{}}}", self.kind.as_i32())); return base; } } /* * Forwarding SQL errors as we can handle those error in caller site for this leaf function * On success we back a Vec which we can JSON'ify later and use as a response */ async fn get_channels_vec(conn: Conn) -> Result, Error> { let rows_db = conn.prep_exec(r"SELECT * FROM channels", ()).await?; let (_, rows) = rows_db.map_and_drop(|row| { let (id, name, desc, kind): (UBigInt, VarChar, Option, Integer) = mysql_async::from_row(row); Channel::from_tup((id, name, desc, kind)) }).await?; Ok(rows) } pub async fn list_channels(pool: &Pool, response: &mut Response) { /* * Primary dispatcher for dealing with the CHANNELS_LIST route * For the most part this function will have a lot more error handling as it * should know what kind of issues its child functions will have */ if let Ok(conn) = pool.get_conn().await { match get_channels_vec(conn).await { Ok(chans) => { *response.status_mut() = StatusCode::OK; response.headers_mut().insert("Content-Type", HeaderValue::from_static("application/json")); // At this point we build the content of our response body // which is a json payload hence why there is weird string manipulation // because we're trying to avoid dependancy issues and serializing things ourselves let mut new_body = String::from("{\"channels\":["); for chan in chans.iter() { let s = format!("{},", chan.as_json_str()); new_body.push_str(&s); } if new_body.ends_with(',') {new_body.pop();} new_body.push_str("]}"); *response.body_mut() = Body::from(new_body); }, Err(_) => { *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; } } } else { *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; } } async fn insert_channel(pool: &Pool, name: &str, desc: &str, kind: i64) -> Result<(), Error>{ let conn = pool.get_conn().await?; conn.prep_exec( "INSERT INTO channels (name, description, kind) VALUES (:name, :description, :kind)", params!{"name" => name, "kind" => kind, "description" => desc}).await?; Ok(()) } pub async fn create_channel(pool: &Pool, response: &mut Response, params: Value) { /* * Create a channel base on a few parameters that may or may not be there */ // Theres an extra un-needed unwrap to be cut out from this proc // specifically with the desc parameter let req_params: (Option<&str>, Option<&str>, Option) = match (params.get("name"), params.get("description"), params.get("kind")) { (Some(name), Some(desc), Some(kind)) => (name.as_str(), desc.as_str(), kind.as_i64()), (Some(name), None, Some(kind)) => (name.as_str(), Some("No Description"), kind.as_i64()), _ => (None, None, None) }; match req_params { (Some(name), Some(desc), Some(kind)) => { match insert_channel(pool, name, desc, kind).await { // Server Errors are generally _ok_ to reveal in body I suppose Err(Error::Server(se)) => { common::db_err_response_body(response, se); //*response.status_mut() = StatusCode::BAD_REQUEST; //let b = format!("Server code: {}\nServer Message:{}", se.code, se.message); //*response.body_mut() = Body::from(b); }, // generic errors get a 500 Err(_) => { *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; } // Nothing to do when things go right _ => {} } }, // basically one of the parameter gets failed so we bail on all of this _ => *response.status_mut() = StatusCode::BAD_REQUEST } } async fn db_delete_channel(pool: &Pool, name: &Value) -> Result<(), Error> { let conn = pool.get_conn().await?; conn.prep_exec(r"DELETE FROM channels WHERE name = :name", params!{"name" => name.as_str().unwrap()}).await?; Ok(()) } pub async fn delete_channel(pool: &Pool, response: &mut Response, params: Value) { // make sure we have the right parameters provided if let Some(name) = params.get("name") { match db_delete_channel(pool, name).await { Ok(_) => *response.status_mut() = StatusCode::OK, Err(e) => { *response.body_mut() = Body::from(format!("delete_chanel sql error :\n{}", e)); } } } else { *response.status_mut() = StatusCode::BAD_REQUEST; } } #[cfg(test)] mod channels_tests { use crate::testing::{get_pool, hyper_resp}; use serde_json::Value; use hyper::StatusCode; const DUMMY_TRANSIENT_CHANNEL: &'static str = "sample channel"; #[tokio::test] async fn list_all_channels_good() { // Generation of data let p = get_pool(); let mut resp = hyper_resp(); // @params: none // Collection of data super::list_channels(&p, &mut resp).await; // Analysis assert_eq!(StatusCode::OK, resp.status()); println!("list_all_channels_good : \t{:?}", resp.body()); let _ = p.disconnect().await; } #[tokio::test] async fn delete_and_create_channel_good() { use chrono::Utc; let p = get_pool(); let mut resp = hyper_resp(); // @params: name + kind + [description] let cname_id = Utc::now(); let params: Value = serde_json::from_str(&format!(r#" {{ "name": "{}-{}", "kind": 2, "description": "some random bs" }} "#, DUMMY_TRANSIENT_CHANNEL, cname_id)).unwrap(); super::create_channel(&p, &mut resp, params).await; println!("CREATE CHANNEL: {:?}", resp.body()); assert_eq!(StatusCode::OK, resp.status()); // clean up and hopefully delete the channel properly let mut resp_delete = hyper_resp(); let params_delete: Value = serde_json::from_str(&format!(r#" {{ "name": "{}-{}" }} "#, DUMMY_TRANSIENT_CHANNEL, cname_id)).unwrap(); println!("Parameters: {}", params_delete); super::delete_channel(&p, &mut resp_delete, params_delete).await; println!("Body: {:?}", resp.body()); let _ = p.disconnect().await; } #[tokio::test] async fn delete_channel_missing_name() { let p = get_pool(); let mut resp = hyper_resp(); // this endpoint is super lenient for some reason btw let param: Value = serde_json::from_str("{}").expect("JSON is not written correctly"); super::delete_channel(&p, &mut resp, param).await; assert_eq!(StatusCode::BAD_REQUEST, resp.status()); } #[tokio::test] async fn delet_channel_non_real_channel() { let p = get_pool(); let mut resp = hyper_resp(); // this endpoint is super lenient for some reason btw let param: Value = serde_json::from_str(r#"{ "name": "this channel doesn't exist" }"#).expect("JSON is not written correctly"); super::delete_channel(&p, &mut resp, param).await; assert_eq!(StatusCode::OK, resp.status()); } }