Organize code, begin database implementation

This commit is contained in:
Elnu 2023-04-14 13:08:41 -07:00
parent 376fba6795
commit 4e9784baf4
7 changed files with 594 additions and 177 deletions

52
src/database.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::Word;
use derive_more::From;
use std::path::PathBuf;
use rusqlite::{Connection, Result, params};
pub struct Database {
conn: Connection,
}
#[derive(From, Debug)]
pub enum DatabaseCreationError {
RusqliteError(rusqlite::Error),
IoError(std::io::Error),
}
impl Database {
pub fn new(
testing: bool,
) -> Result<Self, DatabaseCreationError> {
let conn = if testing {
Connection::open_in_memory()
} else {
let path = PathBuf::from("shiritori.db");
//fs::create_dir_all(path.parent().unwrap())?;
Connection::open(path)
}?;
conn.execute(
"CREATE TABLE IF NOT EXISTS word (
id INTEGER PRIMARY KEY,
word TEXT, reading TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
)",
params![],
)?;
Ok(Self { conn })
}
pub fn load_words_before(&self, before_id: i64) -> Result<Vec<Word>> {
self.conn
.prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? DESC LIMIT 10")?
.query_map(params![before_id], |row| {
Ok(Word {
id: row.get(0)?,
word: row.get(1)?,
reading: row.get(2)?,
timestamp: row.get(3)?,
})
})?
.collect::<Result<Vec<Word>>>()
}
}

20
src/dictionary.rs Normal file
View file

@ -0,0 +1,20 @@
use wana_kana::{ConvertJapanese, IsJapaneseStr};
pub fn lookup(input: &str) -> Option<&jisho::Entry> {
let input = input.trim();
for word in jisho::lookup(input) {
if
// If input has no kanji,
// we can just compare the input to the reading verbatim
// ensuring both are hiragana
(input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) ||
// Otherwise, we have to ensure that the input
// is verbosely the same.
// However, this will cause problems for some words.
// For example, 照り焼き will be accepted but 照焼 won't.
(input == word.kanji) {
return Some(word);
}
}
return None;
}

View file

@ -1,177 +1,18 @@
use simple_websockets::{Event, Responder, Message};
use std::collections::HashMap;
use serde::Serialize;
use wana_kana::{ConvertJapanese, IsJapaneseStr};
mod database;
pub use database::*;
#[derive(Serialize)]
struct MessageResponse {
event: String,
data: MessageResponseData,
}
mod dictionary;
pub use dictionary::*;
impl MessageResponse {
fn to_message(&self) -> Message {
Message::Text(serde_json::to_string(&self).unwrap())
}
}
mod server;
pub use server::*;
#[derive(Serialize)]
#[serde(untagged)]
enum MessageResponseData {
Greeting {
id: u64,
},
Word {
author: u64,
word: String,
reading: Option<String>,
next_char: char,
},
PlayerCount {
players: u64,
},
Error {
message: String,
},
}
impl MessageResponseData {
fn get_name(&self) -> String {
String::from(match self {
Self::Greeting { .. } => "greeting",
Self::Word { .. } => "word",
Self::PlayerCount { .. } => "playerCount",
Self::Error { .. } => "error",
})
}
fn to_response(self) -> MessageResponse {
MessageResponse {
event: self.get_name(),
data: self,
}
}
fn to_message(self) -> Message {
self.to_response().to_message()
}
}
fn broadcast_player_count(clients: &mut HashMap<u64, Responder>) {
let response = MessageResponseData::PlayerCount { players: clients.len() as u64 }.to_response();
for (_client, responder) in clients.iter() {
responder.send(response.to_message());
}
}
fn lookup(input: &str) -> Option<&jisho::Entry> {
let input = input.trim();
for word in jisho::lookup(input) {
if
// If input has no kanji,
// we can just compare the input to the reading verbatim
// ensuring both are hiragana
(input.is_kana() && input.to_hiragana() == word.reading.to_hiragana()) ||
// Otherwise, we have to ensure that the input
// is verbosely the same.
// However, this will cause problems for some words.
// For example, 照り焼き will be accepted but 照焼 won't.
(input == word.kanji) {
return Some(word);
}
}
return None;
}
mod word;
pub use word::*;
fn main() {
let event_hub = simple_websockets::launch(8080)
.expect("failed to listen on port 8080");
let mut clients: HashMap<u64, Responder> = HashMap::new();
let mut next_char: Option<char> = None;
let mut last_response: Option<MessageResponse> = None;
let mut last_client_id: Option<u64> = None;
loop {
match event_hub.poll_event() {
Event::Connect(client_id, responder) => {
println!("A client connected with id #{}", client_id);
responder.send(MessageResponseData::Greeting { id: client_id }.to_message());
if let Some(ref last_response) = last_response {
responder.send(last_response.to_message());
}
clients.insert(client_id, responder);
broadcast_player_count(&mut clients);
},
Event::Disconnect(client_id) => {
println!("Client #{} disconnected.", client_id);
clients.remove(&client_id);
broadcast_player_count(&mut clients);
},
Event::Message(client_id, message) => {
// Ignore binary messages
let message = match message {
Message::Text(message) => message,
Message::Binary(_) => return,
};
// Debug
println!("Received a message from client #{}: {:?}", client_id, message);
let response = if Some(client_id) == last_client_id {
MessageResponseData::Error {
message: String::from("It's not your turn!"),
}
} else {
match lookup(&message) {
Some(entry) => {
if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "" {
MessageResponseData::Error {
message: String::from("Can't end with ん!"),
}
} else if next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == next_char.unwrap() {
next_char = {
// If final character is lengthener or not kana
// Use semifinal
let mut final_chars = entry.reading.chars().rev();
let final_char = final_chars.next().unwrap();
Some(if final_char == 'ー' || !final_char.to_string().is_kana() {
final_chars.next().unwrap()
} else {
final_char
}.to_string().to_hiragana().chars().next().unwrap())
};
MessageResponseData::Word {
author: client_id,
word: entry.kanji.to_owned(),
reading: Some(entry.reading.to_owned()),
next_char: next_char.unwrap(),
}
} else {
MessageResponseData::Error {
message: String::from("Wrong starting kana!"),
}
}
}
None => MessageResponseData::Error {
message: String::from("Not in dictionary!"),
},
}
}.to_response();
match response.data {
// Send errors to only this client
MessageResponseData::Error { .. } => {
clients.get(&client_id).unwrap().send(response.to_message());
},
// Broadcast everything else to all clients
_ => {
for (_client, responder) in clients.iter() {
responder.send(response.to_message());
}
last_response = Some(response);
last_client_id = Some(client_id);
},
}
},
}
}
const PORT: u16 = 8080;
let mut server = Server::new(PORT)
.expect(&format!("Failed to start server at port {PORT}"));
server.run();
}

181
src/server.rs Normal file
View file

@ -0,0 +1,181 @@
use crate::dictionary::lookup;
use simple_websockets::{Event, Responder, Message, EventHub};
use std::collections::HashMap;
use serde::Serialize;
use wana_kana::{ConvertJapanese, IsJapaneseStr};
#[derive(Serialize)]
struct MessageResponse {
event: String,
data: MessageResponseData,
}
impl MessageResponse {
fn to_message(&self) -> Message {
Message::Text(serde_json::to_string(&self).unwrap())
}
}
#[derive(Serialize)]
#[serde(untagged)]
enum MessageResponseData {
Greeting {
id: u64,
},
Word {
author: u64,
word: String,
reading: Option<String>,
next_char: char,
},
PlayerCount {
players: u64,
},
Error {
message: String,
},
}
impl MessageResponseData {
fn get_name(&self) -> String {
String::from(match self {
Self::Greeting { .. } => "greeting",
Self::Word { .. } => "word",
Self::PlayerCount { .. } => "playerCount",
Self::Error { .. } => "error",
})
}
fn to_response(self) -> MessageResponse {
MessageResponse {
event: self.get_name(),
data: self,
}
}
fn to_message(self) -> Message {
self.to_response().to_message()
}
}
pub struct Server {
event_hub: EventHub,
clients: HashMap<u64, Responder>,
next_char: Option<char>,
last_response: Option<MessageResponse>,
last_client_id: Option<u64>,
}
impl Server {
pub fn new(port: u16) -> Result<Self, simple_websockets::Error> {
Ok(Server {
event_hub: simple_websockets::launch(port)?,
clients: HashMap::new(),
next_char: None,
last_response: None,
last_client_id: None,
})
}
pub fn run(&mut self) {
loop {
match self.event_hub.poll_event() {
Event::Connect(client_id, responder) => self.handle_connection(client_id, responder),
Event::Disconnect(client_id) => self.handle_disconnection(client_id),
Event::Message(client_id, message) => self.handle_message(client_id, message),
}
}
}
fn broadcast_player_count(&self) {
let response = MessageResponseData::PlayerCount { players: self.clients.len() as u64 }.to_response();
for (_client, responder) in self.clients.iter() {
responder.send(response.to_message());
}
}
fn handle_connection(&mut self, client_id: u64, responder: Responder) {
println!("A client connected with id #{}", client_id);
responder.send(MessageResponseData::Greeting { id: client_id }.to_message());
if let Some(ref last_response) = self.last_response {
responder.send(last_response.to_message());
}
self.clients.insert(client_id, responder);
self.broadcast_player_count();
}
fn handle_disconnection(&mut self, client_id: u64) {
println!("Client #{} disconnected.", client_id);
self.clients.remove(&client_id);
self.broadcast_player_count();
}
fn handle_message(&mut self, client_id: u64, message: Message) {
// Ignore binary messages
let message = match message {
Message::Text(message) => message,
Message::Binary(_) => return,
};
// Debug
println!("Received a message from client #{}: {:?}", client_id, message);
let response = if Some(client_id) == self.last_client_id {
MessageResponseData::Error {
message: String::from("It's not your turn!"),
}
} else {
match lookup(&message) {
Some(entry) => {
if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "" {
MessageResponseData::Error {
message: String::from("Can't end with ん!"),
}
} else if self.next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == self.next_char.unwrap() {
self.next_char = {
// If final character is lengthener or not kana
// Use semifinal
let mut final_chars = entry.reading.chars().rev();
let final_char = final_chars.next().unwrap();
Some(if final_char == 'ー' || !final_char.to_string().is_kana() {
final_chars.next().unwrap()
} else {
final_char
}.to_string().to_hiragana().chars().next().unwrap())
};
MessageResponseData::Word {
author: client_id,
word: entry.kanji.to_owned(),
reading: Some(entry.reading.to_owned()),
next_char: self.next_char.unwrap(),
}
} else {
MessageResponseData::Error {
message: String::from("Wrong starting kana!"),
}
}
}
None => MessageResponseData::Error {
message: String::from("Not in dictionary!"),
},
}
}.to_response();
match response.data {
// Send errors to only this client
MessageResponseData::Error { .. } => {
self.clients.get(&client_id).unwrap().send(response.to_message());
},
// Broadcast everything else to all clients
_ => {
for (_client, responder) in self.clients.iter() {
responder.send(response.to_message());
}
self.last_response = Some(response);
self.last_client_id = Some(client_id);
},
}
}
}

11
src/word.rs Normal file
View file

@ -0,0 +1,11 @@
use chrono::{DateTime, Utc};
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Word {
pub id: i64,
pub word: String,
pub reading: String,
pub timestamp: DateTime<Utc>,
}