rust-cache-server 0.1.0

High-performance Redis-like cache server in Rust - open source alternative without Redis licensing concerns
Documentation
use std::env;
use std::io::{self, Write};
use std::net::SocketAddr;

use bytes::BytesMut;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

use rust_cache_server::protocol::{encode_frame, parse_frame, RespFrame};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Default host/port same as server: 127.0.0.1:7777
    let mut host = "127.0.0.1".to_string();
    let mut port: u16 = 7777;

    let args: Vec<String> = env::args().collect();
    // Simple CLI:
    // rust-cache-cli [host] [port]
    if args.len() >= 2 {
        host = args[1].clone();
    }
    if args.len() >= 3 {
        if let Ok(p) = args[2].parse::<u16>() {
            port = p;
        }
    }

    let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
    println!("Connecting to {} ...", addr);

    let stream = TcpStream::connect(addr).await?;
    println!("Connected. Type commands like redis-cli, e.g.:");
    println!("  PING");
    println!("  SET foo bar");
    println!("  GET foo");
    println!("Ctrl+C to exit.");

    repl(stream).await?;
    Ok(())
}

async fn repl(mut stream: TcpStream) -> anyhow::Result<()> {
    let mut input = String::new();
    let stdin = io::stdin();
    let mut stdout = io::stdout();
    let mut buf = BytesMut::with_capacity(4096);

    loop {
        input.clear();
        print!("rust-cache> ");
        stdout.flush()?;

        if stdin.read_line(&mut input)? == 0 {
            break;
        }

        let line = input.trim();
        if line.is_empty() {
            continue;
        }

        // Very simple split by whitespace, no quoting.
        let parts: Vec<&str> = line.split_whitespace().collect();
        let frame = RespFrame::Array(Some(
            parts
                .into_iter()
                .map(|p| RespFrame::BulkString(Some(p.as_bytes().to_vec())))
                .collect(),
        ));

        let encoded = encode_frame(&frame);
        stream.write_all(&encoded).await?;

        buf.clear();
        let mut tmp = [0u8; 4096];
        let n = stream.read(&mut tmp).await?;
        if n == 0 {
            println!("\nConnection closed by server.");
            break;
        }
        buf.extend_from_slice(&tmp[..n]);

        match parse_frame(&mut buf) {
            Ok(Some(resp)) => {
                println!("{}", format_resp(&resp));
            }
            Ok(None) => {
                println!("(no reply)");
            }
            Err(e) => {
                println!("(error) {}", e);
            }
        }
    }

    Ok(())
}

fn format_resp(resp: &RespFrame) -> String {
    match resp {
        RespFrame::SimpleString(s) => s.clone(),
        RespFrame::Error(s) => format!("(error) {s}"),
        RespFrame::Integer(i) => format!("(integer) {i}"),
        RespFrame::BulkString(Some(b)) => {
            let s = String::from_utf8_lossy(b);
            s.to_string()
        }
        RespFrame::BulkString(None) => "(nil)".to_string(),
        RespFrame::Array(Some(items)) => {
            let mut out = String::new();
            for (i, item) in items.iter().enumerate() {
                out.push_str(&format!("{}) {}\n", i + 1, format_resp(item)));
            }
            if out.ends_with('\n') {
                out.pop();
            }
            out
        }
        RespFrame::Array(None) => "(nil)".to_string(),
    }
}