freechat/server/src/channels.rs
2020-07-31 22:10:27 -07:00

242 lines
7.8 KiB
Rust

use std::borrow::Cow;
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::{DbError, UBigInt, VarChar, Integer};
#[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,
}
}
// whole ass function exists because serde_json is a walking pos
pub fn from_i64_opt(x: Option<i64>) -> ChannelType {
if let Some(i) = x {
match i {
1 => ChannelType::Voice,
2 => ChannelType::Text,
_ => ChannelType::Undefined
}
}
else {
ChannelType::Undefined
}
}
}
// Primary way of interpretting sql data on our channels table
pub struct Channel {
id: u64,
name: String,
description: String,
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<VarChar>, 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<i32>,
* "name": "<some name here>",
* "description": Option<"<description here>">,
* "kind": kind<i32>
* }
*/
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<Channel> which we can JSON'ify later and use as a response
*/
async fn get_channels_vec(conn: Conn) -> Result<Vec<Channel>, 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<VarChar>, 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<Body>) {
/*
* 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;
}
}
fn parse_insert_channel_params(name_r: Option<&Value>, kind_r: Option<&Value>) ->
Result<InsertableChannel, Error> {
match (name_r, kind_r) {
(Some(name), Some(kind)) => {
let channel = InsertableChannel {
name: name.as_str().unwrap().into(), // as_str removes the quotes from the output
kind: ChannelType::from_i64_opt(kind.as_i64()),
};
println!("Insertable channel name: {}", channel.name);
Ok(channel)
}
_ => {
let x = Cow::from("Missing required parameters");
Err(Error::Other(x))
}
}
}
async fn insert_channel(pool: &Pool, channel: InsertableChannel) -> Result<(), DbError> {
if let Ok(conn) = pool.get_conn().await {
let db_result: Result<Conn, Error> = conn.drop_exec(
"INSERT INTO channels (name, kind) VALUES (:name, 0)",
params!{"name" => channel.name}
).await;
match db_result {
Ok(_) => Ok(()),
Err(e) => {
match e {
Error::Server(_) => Err(DbError::BadParam("Couldn't process input somehow")),
_ => Err(DbError::Internal)
}
}
}
}
else {
Err(DbError::Connection("Could not connect to database at all"))
}
}
pub async fn create_channel(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* Create a channel base on a few parameters that may or may not be there
*/
let name_r = params.get("name");
let kind_r = params.get("kind");
match parse_insert_channel_params(name_r, kind_r) {
Ok(channel) => {
match insert_channel(pool, channel).await {
Ok(_) => *response.status_mut() = StatusCode::OK,
Err(dbe) => {
// Todo: add some form of loggin off the back of these values
match dbe {
DbError::BadParam(_msg) => *response.status_mut() = StatusCode::BAD_REQUEST,
DbError::Connection(_msg) => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR,
DbError::Internal => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
}
}
}
},
Err(_) => *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<Body>, 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) => {
println!("delete_chanel sql error :\n{}", e);
}
}
}
else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}