Skip to content

Commit

Permalink
New CLI (#26)
Browse files Browse the repository at this point in the history
* cli progress

* mvp

* tweak message

* apply dumb clippy suggestion
  • Loading branch information
immanelg authored Mar 7, 2024
1 parent b50d535 commit 8832327
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 120 deletions.
1 change: 1 addition & 0 deletions memcrab-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ repository = "https://github.com/cospectrum/memcrab"
anyhow = "1.0.79"
clap = { version = "4.4.12", features = ["derive"] }
memcrab = { version = "0.1.0", path = "../memcrab" }
memcrab-server = { version = "0.1.0", path = "../memcrab-server" }
rustyline = "13.0.0"
tokio = { version = "1.35.1", features = ["full"] }
23 changes: 11 additions & 12 deletions memcrab-cli/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# memcrab-cli

Command line interface for memcrab
Command line interface for memcrab.

# Usage

Start a server
## Server
Start a server on TCP address `127.0.0.1:4949`
```bash
cargo run -- -H 127.0.0.1 -p 6969 -s &
memcrab-cli server -a 127.0.0.1:4949 &
```

Connect client to a running server
## Client
Execute one and exit
```bash
cargo run -- -H 127.0.0.1 -p 6969
memcrab-cli client -a 127.0.0.1:4949 set key value
```

This will start an interactive session. You can use `get` and `set` commands.

```
memcrab> set x 1 2 3
memcrab> get x
x: [1, 2, 3]
memcrab>
Start interactive REPL. Press Ctrl-d to exit.
```bash
memcrab-cli client -a 127.0.0.1:4949
```

Empty file added memcrab-cli/src/client.rs
Empty file.
261 changes: 153 additions & 108 deletions memcrab-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,108 +1,153 @@
// use anyhow::bail;
// use clap::Parser;
// use core::num::NonZeroUsize;
// use memcrab::RawClient;
// use memcrab_cache::Cache;
// use rustyline::error::ReadlineError;
// use rustyline::DefaultEditor;
//
// #[derive(Parser)]
// #[command(author, version, about, long_about = None)]
// struct Cli {
// #[arg(short = 'H', long, default_value = "127.0.0.1")]
// host: String,
//
// #[arg(short, long, default_value = "9090")]
// port: String,
//
// #[arg(short, long, action)]
// server: bool,
// }
//
// async fn eval_lines(addr: String) -> anyhow::Result<()> {
// let addr = addr.parse()?;
// let mut client = RawClient::connect(addr).await?;
// let mut editor = DefaultEditor::new()?;
// loop {
// let line = editor.readline("memcrab> ");
// match line {
// Ok(line) => {
// eval_line(&mut client, line)
// .await
// .unwrap_or_else(|e| println!("error: {:?}", e));
// }
// Err(ReadlineError::Interrupted) => {
// println!("Ctrl-c");
// }
// Err(ReadlineError::Eof) => {
// println!("quit");
// break;
// }
// Err(err) => {
// println!("error: {:?}", err);
// break;
// }
// }
// }
//
// Ok(())
// }
//
// async fn eval_line(client: &mut RawClient, line: String) -> anyhow::Result<()> {
// let tokens = line.split_whitespace().collect::<Vec<_>>();
// if tokens.is_empty() {
// return Ok(());
// }
// if tokens[0] == "get" {
// if tokens.len() != 2 {
// bail!("syntax error: expected one key after `get`");
// }
// let resp = client.get(tokens[1]).await?;
// match resp {
// Some(val) => println!("{}: {:?}", tokens[1], val),
// None => println!("no value set"),
// }
// } else if tokens[0] == "set" {
// if tokens.len() < 3 {
// bail!("syntax error: expected one key and bytes after `set`");
// }
//
// client
// .set(
// tokens[1],
// tokens[2..]
// .iter()
// .map(|&s| s.parse().unwrap())
// .collect::<Vec<u8>>(),
// )
// .await?;
// } else {
// bail!("syntax error: unexpected token {}", tokens[0]);
// }
// Ok(())
// }
//
// #[allow(unused)]
// async fn serve(addr: String) -> anyhow::Result<()> {
// let maxbytes = 100_000;
// let maxlen = NonZeroUsize::new(110).unwrap();
// let cache = Cache::new(maxlen, maxbytes);
//
// todo!("server is not implemented");
// Ok(())
// }
//
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// let cli = Cli::parse();
//
// let addr = format!("{}:{}", cli.host, cli.port);
//
// if cli.server {
// serve(addr).await?;
// } else {
// eval_lines(addr).await?;
// }
// Ok(())
// }
fn main() {}
use anyhow::anyhow;
use clap::{Parser, Subcommand};
use memcrab::{connections::Tcp, RawClient};
use std::net::SocketAddr;

#[derive(Parser)]
#[command(author, version, about = "command line interface for memcrab (server and client)", long_about = None)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
enum Commands {
Server {
// #[arg(short = 'H', long, default_value = "127.0.0.1")]
// host: Ipv4Addr,
//
// #[arg(short, long, default_value = "9090")]
// port: u16,

// TODO: Take either SocketAddr for TCP or PathBuf for UNIX Socket
#[arg(short, long, default_value = "127.0.0.1:9090")]
address: SocketAddr,
},
Client {
// #[arg(short = 'H', long, default_value = "127.0.0.1")]
// host: Ipv4Addr,
//
// #[arg(short, long, default_value = "9090")]
// port: u16,
#[arg(short, long, default_value = "127.0.0.1:9090")]
address: SocketAddr,
// #[clap(trailing_var_arg = true, allow_hyphen_values = true, help = "execute command and exit")]
// cmd: Vec<String>,
},
}

fn tokenize_line(line: String) -> anyhow::Result<Vec<String>> {
let mut tokens = Vec::new();
let mut word = String::new();
let mut quote = false;

for c in line.chars() {
match c {
'"' if quote => {
tokens.push(word.clone());
word.clear();
quote = false;
}
'"' => {
quote = true;
}
' ' if quote => {
word.push(c);
}
' ' => {
if !word.is_empty() {
tokens.push(word.clone());
word.clear();
}
}
c => {
word.push(c);
}
}
}
if quote {
return Err(anyhow!("syntax error"));
}

if !word.is_empty() {
tokens.push(word);
}

Ok(tokens)
}

async fn eval_line<C: memcrab::Rpc>(
client: &mut RawClient<C>,
line: String,
) -> anyhow::Result<String> {
let tokens = tokenize_line(line)?;
match tokens.first().map(|s| s.as_ref()) {
Some("get") if tokens.len() == 2 => {
let value = client.get(&tokens[1]).await?;
match value {
Some(value) => {
let string_value = String::from_utf8(value).unwrap();
Ok(string_value)
}
None => Ok(String::from("key not found")),
}
}
Some("set") if tokens.len() == 3 => {
client.set(&tokens[1], tokens[2].to_owned().into()).await?;
Ok(String::from("ok"))
}
_ => Err(anyhow!("syntax error")),
}
}

async fn serve_memcrab(addr: SocketAddr) -> anyhow::Result<()> {
use memcrab_server::{serve, Cache};
use tokio::net::TcpListener;

let gb = 2_usize.pow(30);
let cache = Cache::builder()
.segments(10)
.max_bytesize(gb)
.build()
.into();

let listener = TcpListener::bind(addr).await.unwrap();
serve(listener, cache).await?;
Ok(())
}

async fn repl<C: memcrab::Rpc>(mut client: RawClient<C>) -> anyhow::Result<()> {
let mut editor = rustyline::DefaultEditor::new()?;

for readline in editor.iter("📝🦀 ") {
match readline {
Ok(line) => match eval_line(&mut client, line).await {
Ok(message) => println!("{}", message),
Err(err) => println!("error: {}", err),
},
Err(rustyline::error::ReadlineError::Eof) => {
println!("quit");
break;
}
Err(e) => println!("error: {}", e),
}
}
Ok(())
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();

match cli.command {
Commands::Client { address } => {
let client = RawClient::<Tcp>::connect(address).await?;
repl(client).await?;
}
Commands::Server { address } => {
serve_memcrab(address).await?;
}
}

Ok(())
}
Empty file added memcrab-cli/src/server.rs
Empty file.

0 comments on commit 8832327

Please sign in to comment.