Update src/main.rs

This commit is contained in:
Григорий Сафронов 2025-05-25 21:28:01 +00:00
parent 88e7c6c7b6
commit 7663c4277a

View File

@ -1,18 +1,41 @@
use std::io::{self, Write}; use std::io::{self, Write};
use std::net::{TcpStream, ToSocketAddrs}; use std::net::TcpStream;
use std::time::Duration; use std::time::Duration;
use std::env; use std::env;
use colored::*; use colored::Colorize;
use regex::Regex; use regex::Regex;
use std::process;
mod resp; mod resp;
const DEFAULT_PORT: u16 = 9880; const DEFAULT_PORT: u16 = 9880;
const PROMPT_NAME: &str = "futriix"; const PROMPT_NAME: &str = "futriix";
const CONNECTION_TIMEOUT: u64 = 5; // seconds const CONNECTION_TIMEOUT_SECS: u64 = 2;
fn main() { fn main() {
// Parse command line arguments let (host, port) = parse_args();
let addr = format!("{}:{}", host, port);
let stream = match TcpStream::connect_timeout(
&addr.parse().unwrap(),
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
) {
Ok(stream) => stream,
Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
eprintln!("{}", "Connection refused".red());
process::exit(1);
},
Err(e) => {
eprintln!("Connection error: {}", e);
process::exit(1);
}
};
println!("Connected to {}", addr.green());
run_repl_loop(stream, &host, port);
}
fn parse_args() -> (String, u16) {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
let mut host = "127.0.0.1".to_string(); let mut host = "127.0.0.1".to_string();
let mut port = DEFAULT_PORT; let mut port = DEFAULT_PORT;
@ -28,94 +51,63 @@ fn main() {
} }
} }
} }
(host, port)
}
// Try to connect to server fn run_repl_loop(stream: TcpStream, host: &str, port: u16) {
let addr = format!("{}:{}", host, port);
let socket_addr = match format!("{}:{}", host, port).to_socket_addrs() {
Ok(mut addrs) => addrs.next().expect("No address found"),
Err(_e) => {
eprintln!("Failed to resolve address {}", addr);
return;
}
};
let stream = match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(CONNECTION_TIMEOUT)) {
Ok(s) => s,
Err(_e) => {
eprintln!("Connection to port {} refused", port);
return;
}
};
println!("Connected to {}", addr);
// Main REPL loop
let mut input = String::new(); let mut input = String::new();
loop { loop {
// Print prompt print_prompt(host, port);
print_prompt(&host, port);
// Read input
input.clear(); input.clear();
io::stdin().read_line(&mut input).expect("Failed to read input");
let input = input.trim(); if io::stdin().read_line(&mut input).is_err() {
eprintln!("{}", "Failed to read input".red());
continue;
}
let input = input.trim();
if input.is_empty() { if input.is_empty() {
continue; continue;
} }
// Handle special commands
if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") { if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
break; break;
} }
// Validate command before sending
if !is_valid_command(input) { if !is_valid_command(input) {
eprintln!("Error: Invalid command format"); eprintln!("{}", "Error: Invalid command format".red());
continue; continue;
} }
// Send command to server
match send_command(&stream, input) { match send_command(&stream, input) {
Ok(response) => { Ok(response) => print_response(&response),
print_response(&response);
}
Err(e) => { Err(e) => {
eprintln!("Error: {}", e.to_string().replace("KeyDB", "Futriix")); if is_connection_error(&e) {
// Check if connection was lost eprintln!("{}", "Connection error".red());
if e.kind() == io::ErrorKind::ConnectionAborted ||
e.kind() == io::ErrorKind::ConnectionReset {
eprintln!("Connection lost. Please restart the client.");
break; break;
} }
eprintln!("{}", format!("Error: {}", e.to_string().replace("KeyDB", "Futriix")).red());
} }
} }
} }
} }
fn is_valid_command(cmd: &str) -> bool {
if cmd.is_empty() {
return false;
}
if cmd.chars().any(|c| c.is_control()) {
return false;
}
cmd.split_whitespace().next().is_some()
}
fn print_prompt(host: &str, port: u16) { fn print_prompt(host: &str, port: u16) {
let prompt = format!("{}:{}:{}:~>", PROMPT_NAME, host, port); let prompt = format!("{}:{}:{}:~>", PROMPT_NAME, host, port);
print!("{} ", prompt.green()); print!("{} ", prompt.green());
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
} }
fn is_valid_command(cmd: &str) -> bool {
!cmd.is_empty() &&
!cmd.chars().any(|c| c.is_control()) &&
cmd.split_whitespace().next().is_some()
}
fn send_command(stream: &TcpStream, command: &str) -> io::Result<resp::Value> { fn send_command(stream: &TcpStream, command: &str) -> io::Result<resp::Value> {
let parts: Vec<&str> = command.split_whitespace().collect(); let parts: Vec<&str> = command.split_whitespace().collect();
let mut resp_command = String::new(); let mut resp_command = format!("*{}\r\n", parts.len());
resp_command.push_str(&format!("*{}\r\n", parts.len()));
for part in parts { for part in parts {
resp_command.push_str(&format!("${}\r\n{}\r\n", part.len(), part)); resp_command.push_str(&format!("${}\r\n{}\r\n", part.len(), part));
} }
@ -130,10 +122,10 @@ fn send_command(stream: &TcpStream, command: &str) -> io::Result<resp::Value> {
fn print_response(value: &resp::Value) { fn print_response(value: &resp::Value) {
match value { match value {
resp::Value::SimpleString(s) | resp::Value::BulkString(s) => { resp::Value::SimpleString(s) | resp::Value::BulkString(s) => {
println!("{}", s.replace("KeyDB", "Futriix")) println!("{}", s.replace("KeyDB", "Futriix"));
}, },
resp::Value::Error(e) => { resp::Value::Error(e) => {
println!("(error) {}", e.replace("KeyDB", "Futriix")) println!("{}", format!("(error) {}", e.replace("KeyDB", "Futriix")).red());
}, },
resp::Value::Integer(i) => println!("(integer) {}", i), resp::Value::Integer(i) => println!("(integer) {}", i),
resp::Value::Array(arr) => { resp::Value::Array(arr) => {
@ -141,17 +133,16 @@ fn print_response(value: &resp::Value) {
print!("{}) ", i + 1); print!("{}) ", i + 1);
print_response(item); print_response(item);
} }
} },
resp::Value::Null => println!("(nil)"), resp::Value::Null => println!("(nil)"),
} }
} }
#[cfg(test)] fn is_connection_error(error: &io::Error) -> bool {
mod tests { matches!(
use super::*; error.kind(),
io::ErrorKind::ConnectionAborted |
#[test] io::ErrorKind::ConnectionReset |
fn test_print_prompt() { io::ErrorKind::BrokenPipe
print_prompt("127.0.0.1", 9880); )
}
} }