A Command Line Key-Value Data Store using the Rust Programming Language.
Tags: rust, cli, cargo, database
Published: 2021-06-15 00:24:08

## Prelude
Rust is an imperative, super fast, and type-safe programming language that empowers you — a Software Engineer — "to reach farther, to program with confidence in a wider variety of domains than you did before." No wonder it has consistently maintained its deserved spot [as the most loved programming language](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved) for half a decade!
## What we are building
Using barebone `rust` code, we will be building a simple command line `key-value data store` like `Redis`. It should take in two command line arguments and assign the first as the `key` while the second, `value`.

If installed on your machine, it can be used as follows:

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ rust-kvstore needle haystack
```
This should create a file, aptly named `kv.db`. It's content can then be read:

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cat kv.db
```
Whose output should look like:
```bash
───────┬─────────────────────────────────────────────────────
       │ File: kv.db
───────┼─────────────────────────────────────────────────────
   1   │ needle  haystack
───────┴─────────────────────────────────────────────────────
```

However, if you have the source files, you can simply `build` and `run` it using:

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cargo run needle haystack
```
This is simply for learning sake and no other motive is intended.

### DECLAIMER
This example is [based off of](https://www.merriam-webster.com/words-at-play/based-on-vs-based-off) a [two-part tutorial anchored by the beloved Ryan Levick](https://www.youtube.com/watch?v=WnWGO-tLtLA). The only significant additions are: the use of a `Vec<String>` instead of `Iterator<Item = String>`; fixing this bug
```bash
thread 'main' panicked at 'Corrupt database: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
``` by checking if `kv.db` already exists and if not create it using Rust's [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html) standard library; using a [more efficient](https://doc.rust-lang.org/std/fs/struct.File.html) file reader; and using [split_once()](https://doc.rust-lang.org/std/primitive.str.html#method.split_once) instead of [rsplit()](https://doc.rust-lang.org/std/primitive.str.html#method.rsplit) among others.
**It is highly recommended to [check out the awesome livestreams on youtube](https://www.youtube.com/watch?v=WnWGO-tLtLA)**. I must confess, it was a total tear down and dissection.

## Assumptions
It is assumed that you have read [The Rust book](https://doc.rust-lang.org/stable/book/) to some extent or have [checked out the awesome livestreams on youtube by Ryan Levick](https://www.youtube.com/watch?v=WnWGO-tLtLA).

### Source code
As usual, you can get the full version of the source files for this article on {% github Sirneij/rust-kvstore %}. Just clone it:
```bash
┌──(sirneij@sirneij)-[~/Documents/Projects]
└─$[sirneij@sirneij Projects]$  git clone https://github.com/Sirneij/rust-kvstore.git
```
and open it in your favourite text editor, mine is [vs code](https://code.visualstudio.com/).
```bash
┌──(sirneij@sirneij)-[~/Documents/Projects]
└─$[sirneij@sirneij Projects]$ cd rust-kvstore && code .
```

## Proper implementation

Going by the assumptions made above, I will only point out some of my inputs. 

  - **Taking arguments as vectors:**

Since our little project wants to get two command line arguments, Rust provides a function `args()` which can be found in the `std::env` library. This function returns an iterator of the command line arguments. The `.collect()` converts the returned iterator into a `vector`. Its implementation for this project looks this way:
```rust
fn main(){
    let args: Vec<String> = std::env::args().collect();
    let key = &args[1];
    let value = &args[2];
}
```
It should be noted that `&args[0]` gives the path to our `executable` which in this case should be `"target/debug/rust-kvstore"`. You can see what is in `args` by printing it to the console using `println!()` macro:

```rust
fn main(){
    let args: Vec<String> = std::env::args().collect();
    println!("{:?}", args);
    let key = &args[1];
    let value = &args[2];
}
```
You should see something like:
```rust
["target/debug/rust-kvstore", "needle", "haystack"]
```
if you pass `needle haystack` as arguments using `cargo run` like so:

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cargo run needle haystack
```
Since we are only concerned with arguments we passed, which starts from `&args[1]`, we overlooked `&args[0]`.

  - **Fixing "No such file or directory" bug:**
If `kv.db` is not manually created or not present at the start of the program's usage, an error of this form will surface:
```bash
thread 'main' panicked at 'Corrupt database: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
To fix this, we need to check whether or not `kv.db` has been created. If not create it on the fly. To accomplish this, we use this awesome `std::path::PathBuf` library in our "constructor", `new()`.
```rust
impl Database {
   fn new() -> Result<Database, std::io::Error> {
        ...
        let mut contents = String::new();
        let path = PathBuf::from("kv.db");
        if path.exists() {
            let file = std::fs::File::open(path)?;
            let mut buf_reader = std::io::BufReader::new(file);
            buf_reader.read_to_string(&mut contents)?;
        } else {
            std::fs::File::create("kv.db")?;
        }
        ...
   }
}
```

  - **Using the more efficient `std::io::BufReader`**
It can also be seen in the snippet above that instead of using the `std::read_to_string`, we opted for the more efficient `std::io::BufReader::new(file);`

  - **split_once() implemented:**
When Ryan was livestreaming, `split_once()` was only available in the `nightly` version of Rust so he opted for `rsplit()`. However, I think it is stable now and the full implementation is shown as follows:
```rust
     ...
     for line in contents.lines() {
            let (key, value) = line.split_once("\t").expect("Corrupt database");
            map.insert(key.to_string(), value.to_string());
        }
     ...
```

That's it! Nifty and awesome. To build the project from scratch, [code along with Ryan Levick.](https://www.youtube.com/watch?v=WnWGO-tLtLA).

Kindly drop your comments, reactions and suggestions. Make me a better writer.

## References
   - [Introduction to Rust](https://www.youtube.com/watch?v=WnWGO-tLtLA) - <cite>by Ryan Levick</cite>
   - [The Rust Programming Language](https://doc.rust-lang.org/stable/book/) - <cite>by Steve Klabnik and Carol Nichols, with contributions from the Rust Community</cite>
   - [Stack Overflow Developer Survey - Most Loved, Dreaded, and Wanted Languages](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved)

## Attributions
   - Cover image made by [Flat Icons](https://www.flaticon.com/authors/flat-icons)