To aid in my learning of Rust, I decided to implement the Java RocksDB example in Rust seeing it is simple enough.

It didn't turn out as simple as I hoped owing to Rust's strict borrowing and lifetime concepts - which almost everyone trips over sooner or later. I originally wanted to directly store json::value::JsonValue enums serialized with bincode. The main errors I encountered were:

the trait `serde::ser::Serialize` is not implemented for `json::value::JsonValue`
...
the trait `serde::de::Deserialize<'_>` is not implemented for `json::value::JsonValue`

I'm sure I could have implemented the traits for that enum if I put in the effort, but seeing as this was supposed to be a simple throwaway example it was fast becoming more than I signed up for.
I decided to work with strings at the repository layer and leave de/serialization to users because I'm lazy like that. ¯\_(ツ)_/¯

We only need json, actix-web, bytes (to read POST bodies) and rocksdb as dependencies:

[dependencies]
actix-rt = "1.0"
actix-web = "2.0"
bytes = "0.5.2"
env_logger = "0.7.1"
json = "0.12"
rocksdb = "0.13.0"

Then we create our repository module src/kv.rs:

// src/kv.rs
use rocksdb::DB;
use std::sync::Arc;

pub trait KVStore {
    fn init(file_path: &str) -> Self;
    fn save(&self, k: &str, v: &str) -> bool;
    fn find(&self, k: &str) -> Option<String>;
    fn delete(&self, k: &str) -> bool;
}

#[derive(Clone)]
pub struct RocksDB {
    db: Arc<DB>,
}


impl KVStore for RocksDB {
    fn init(file_path: &str) -> Self {
        RocksDB { db: Arc::new(DB::open_default(file_path).unwrap()) }
    }

    fn save(&self, k: &str, v: &str) -> bool {
        self.db.put(k.as_bytes(), v.as_bytes()).is_ok()
    }

    fn find(&self, k: &str) -> Option<String> {
        match self.db.get(k.as_bytes()) {
            Ok(Some(v)) => {
                let result = String::from_utf8(v).unwrap();
                println!("Finding '{}' returns '{}'", k, result);
                Some(result)
            },
            Ok(None) => {
                println!("Finding '{}' returns None", k);
                None
            },
            Err(e) => {
                println!("Error retrieving value for {}: {}", k, e);
                None
            }
        }
    }

    fn delete(&self, k: &str) -> bool {
        self.db.delete(k.as_bytes()).is_ok()
    }
}

We declare our key-value repository interface (or traits in Rust) KVStore. The RocksDB struct is to hold the opened database we'll be working with. We wrap DB in Arc so it can be shared safely across threads. We want Actix-web to pass our database handle to our route handlers when they're called, but DB does not implement Clone, and so has to be wrapped in Arc to work.

We then move on to implement our trait (or interface) for RocksDB. RocksDB only works with bytes so we have to convert our string keys and values to bytes.

Next, we'll write our route handler module src/kv_handler.rs:

// src/kv_handler.rs
use actix_web::{web::{Data, Path}, HttpResponse};
use bytes::Bytes;
use json::parse;

use crate::kv::{KVStore, RocksDB};

// curl -i -X GET -H "Content-Type: application/json" http://localhost:8080/api/foo
pub async fn get(key: Path<String>, db: Data<RocksDB>) -> HttpResponse {
    match &db.find(&key.into_inner()) {
        Some(v) => {
            parse(v)
                .map(|obj| HttpResponse::Ok().content_type("application/json").body(obj.dump()))
                .unwrap_or(HttpResponse::InternalServerError().content_type("application/json").finish())
        }
        None    => HttpResponse::NotFound().content_type("application/json").finish()
    }
}

// curl -i -X POST -H "Content-Type: application/json" -d '{"bar":"baz"}' http://localhost:8080/api/foo
pub async fn post(key:  Path<String>,
                  db:   Data<RocksDB>,
                  body: Bytes) -> HttpResponse {
    match String::from_utf8(body.to_vec()) {
        Ok(body) => match &db.save(&key.into_inner(), &body) {
            true  => {
                parse(&body)
                    .map(|obj| HttpResponse::Ok().content_type("application/json").body(obj.dump()))
                    .unwrap_or(HttpResponse::InternalServerError().content_type("application/json").finish())
            }
            false => HttpResponse::InternalServerError().content_type("application/json").finish()
        }
        Err(_) => HttpResponse::InternalServerError().content_type("application/json").finish(),
    }
}

// curl -i -X DELETE -H "Content-Type: application/json" http://localhost:8080/api/foo
pub async fn delete(key: Path<String>, db: Data<RocksDB>) -> HttpResponse {
    match &db.delete(&key.into_inner()) {
        true  => HttpResponse::NoContent().content_type("application/json").finish(),
        false => HttpResponse::InternalServerError().content_type("application/json").finish()
    }
}

Each route takes the key path param and a handle to the database handle we opened earlier.

When we receive a GET request, we query RocksDB with the provided key. If a value is returned, we parse it to JSON and return. We return 404 if we found nothing.

When we receive a POST request, we convert the supplied body to a string and store in RocksDB. We then parse the body to JSON and send back as response.

For DELETE requests, we return 204 if we were able to delete the key-value pair or 500 otherwise.

All that's left is to initialize our database and register our route handlers. Let's handle that in src/main.rs:

// src/main.rs
mod kv;
mod kv_handler;


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{
        middleware::Logger,
        web::{scope, resource, get, post, delete},
        App,
        HttpServer
    };

    let db: kv::RocksDB = kv::KVStore::init("/tmp/rocks/actix-db");

    std::env::set_var("RUST_LOG", "actix_web=info,actix_server=info");
    env_logger::init();

    HttpServer::new(move || {
        App::new()
            .data(db.clone())
            .wrap(Logger::default())
            .service(
                scope("/api")
                .service(
                    resource("/{key}")
                        .route(get().to(kv_handler::get))
                        .route(post().to(kv_handler::post))
                        .route(delete().to(kv_handler::delete)),
                ),
            )
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

The source code can be found here.