├── docs └── images │ └── .gitkeep ├── src ├── main.rs ├── cats │ ├── mod.rs │ ├── handler.rs │ └── repository.rs ├── lib.rs └── mongo_connection.rs ├── Rocket.toml ├── .gitignore ├── benches └── my_benchmark.rs ├── Cargo.toml ├── LICENSE ├── README.md └── tests └── test.rs /docs/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use rustlang_rocket_mongodb::rocket; 2 | 3 | fn main() { 4 | rocket().launch(); 5 | } 6 | -------------------------------------------------------------------------------- /Rocket.toml: -------------------------------------------------------------------------------- 1 | [development] 2 | address = "localhost" 3 | port = 8001 4 | workers = 8 5 | log = "normal" 6 | limits = { forms = 32768 } 7 | 8 | [staging] 9 | address = "0.0.0.0" 10 | port = 80 11 | workers = 8 12 | log = "normal" 13 | limits = { forms = 32768 } 14 | 15 | [production] 16 | address = "0.0.0.0" 17 | port = 80 18 | workers = 8 19 | log = "critical" 20 | limits = { forms = 32768 } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | /.env 4 | 5 | # Generated by Cargo 6 | # will have compiled files and executables 7 | /target/ 8 | 9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 11 | Cargo.lock 12 | 13 | # These are backup files generated by rustfmt 14 | **/*.rs.bk -------------------------------------------------------------------------------- /benches/my_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | // See https://bheisler.github.io/criterion.rs/book/getting_started.html 3 | #[inline] 4 | fn fibonacci(n: u64) -> u64 { 5 | match n { 6 | 0 => 1, 7 | 1 => 1, 8 | n => fibonacci(n - 1) + fibonacci(n - 2), 9 | } 10 | } 11 | 12 | pub fn criterion_benchmark(c: &mut Criterion) { 13 | c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); 14 | } 15 | 16 | criterion_group!(benches, criterion_benchmark); 17 | criterion_main!(benches); 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustlang-rocket-mongodb" 3 | version = "0.1.0" 4 | authors = ["Louis Beaumont "] 5 | 6 | repository = "https://github.com/louis030195/rustlang-rocket-mongodb" 7 | readme = "README.md" 8 | license = "MIT" 9 | edition = "2018" 10 | 11 | 12 | [dependencies] 13 | mongodb = "0.3.11" 14 | dotenv = "0.13.0" 15 | r2d2 = "0.8.3" 16 | r2d2-mongodb = "*" 17 | rocket = "0.4" 18 | rocket_codegen = "0.4" 19 | serde = { version = "1", features = ["derive"] } 20 | serde_derive = "1" 21 | serde_json = "1" 22 | 23 | # Benches 24 | criterion = "0.3" 25 | 26 | [dependencies.rocket_contrib] 27 | default-features = false 28 | features = ["json"] 29 | version = "0.4" 30 | 31 | [[bench]] 32 | name = "my_benchmark" 33 | harness = false -------------------------------------------------------------------------------- /src/cats/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(proc_macro_derive_resolution_fallback)] 2 | 3 | pub mod handler; 4 | pub mod repository; 5 | use mongodb::bson; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct Cat { 9 | #[serde(rename = "_id")] // Use MongoDB's special primary key field name when serializing 10 | pub id: Option, 11 | pub name: Option, 12 | pub color: Option, 13 | pub age: Option, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug, Clone)] 17 | pub struct InsertableCat { 18 | pub name: Option, 19 | pub color: Option, 20 | pub age: Option, 21 | } 22 | 23 | impl InsertableCat { 24 | fn from_cat(cats: Cat) -> InsertableCat { 25 | InsertableCat { 26 | name: cats.name, 27 | color: cats.color, 28 | age: cats.age, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Louis Beaumont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(decl_macro, proc_macro_hygiene)] 2 | #[macro_use] 3 | extern crate rocket; 4 | extern crate dotenv; 5 | extern crate mongodb; 6 | extern crate r2d2; 7 | extern crate r2d2_mongodb; 8 | extern crate rocket_contrib; 9 | #[macro_use] 10 | extern crate serde_derive; 11 | extern crate serde_json; 12 | 13 | use dotenv::dotenv; 14 | use rocket::{Request, Rocket}; 15 | pub mod cats; 16 | mod mongo_connection; 17 | 18 | #[catch(500)] 19 | fn internal_error() -> &'static str { 20 | "Whoops! Looks like we messed up." 21 | } 22 | 23 | #[catch(400)] 24 | fn not_found(req: &Request) -> String { 25 | format!("I couldn't find '{}'. Try something else?", req.uri()) 26 | } 27 | 28 | pub fn rocket() -> Rocket { 29 | dotenv().ok(); 30 | rocket::ignite() 31 | .register(catchers![internal_error, not_found]) 32 | .manage(mongo_connection::init_pool()) 33 | .mount( 34 | "/cats", 35 | routes![ 36 | cats::handler::all, 37 | cats::handler::get, 38 | cats::handler::post, 39 | cats::handler::put, 40 | cats::handler::delete, 41 | cats::handler::delete_all 42 | ], 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rustlang-rocket-mongodb 2 | [![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/louis030195/rustlang-rocket-mongodb) 3 | 4 | Code for the [tutorial](https://medium.com/@louis.beaumont/rest-api-with-rust-mongodb-10eeb6bd51d7) 5 | 6 | ## Installation 7 | 8 | ```bash 9 | sudo apt update 10 | sudo apt install mongodb-org 11 | ``` 12 | 13 | Check if mongodb is healthy 14 | 15 | ```bash 16 | service mongodb status 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```bash 22 | echo -e "MONGO_ADDR=localhost 23 | DB_NAME=rustlang-rocket-mongodb 24 | MONGO_PORT=27017" > .env 25 | ``` 26 | 27 | 28 | ```bash 29 | rustup default nightly # Pear requires a nightly or dev version of Rust 30 | ``` 31 | 32 | ```bash 33 | cargo run & 34 | 35 | # POST 36 | curl -d '{"name": "chichi"}' -H "Content-Type: application/json" -X POST http://localhost:8001/cats 37 | # Or 38 | curl -d '{"name": "chacha", "age": 12, "color": "grey"}' \ 39 | -H "Content-Type: application/json" -X POST http://localhost:8001/cats 40 | # Or empty 41 | curl -d '{}' -H "Content-Type: application/json" -X POST http://localhost:8001/cats 42 | 43 | # PUT 44 | curl -d '{"$oid": "5db15a686539303d5708901f", "name": "chichi"}' -H "Content-Type: application/json" \ 45 | -X PUT http://localhost:8001/cats/5db15a686539303d5708901f 46 | 47 | # GET 48 | curl http://localhost:8001/cats 49 | # Find by id 50 | curl http://localhost:8001/cats/5db15a1f6539303d5708901e 51 | 52 | # DELETE 53 | curl -H "Content-Type: application/json" -X DELETE http://localhost:8001/cats/5db15a1f6539303d5708901e 54 | 55 | # DELETE all 56 | curl -H "Content-Type: application/json" -X DELETE http://localhost:8001/cats 57 | ``` 58 | 59 | ## Tests 60 | 61 | To avoid running parallel tests we use --test-threads=1 because we modify database, otherwise tests would fail. 62 | 63 | ```rust 64 | cargo test -- --test-threads=1 65 | ``` -------------------------------------------------------------------------------- /src/mongo_connection.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use r2d2::PooledConnection; 3 | use r2d2_mongodb::{ConnectionOptions, MongodbConnectionManager}; 4 | use rocket::http::Status; 5 | use rocket::request::{self, FromRequest}; 6 | use rocket::{Outcome, Request, State}; 7 | use std::env; 8 | use std::ops::Deref; 9 | 10 | type Pool = r2d2::Pool; 11 | 12 | pub struct Conn(pub PooledConnection); 13 | 14 | /* 15 | create a connection pool of mongodb connections to allow a lot of users to modify db at same time. 16 | */ 17 | pub fn init_pool() -> Pool { 18 | dotenv().ok(); 19 | let mongo_addr = env::var("MONGO_ADDR").expect("MONGO_ADDR must be set"); 20 | let mongo_port = env::var("MONGO_PORT").expect("MONGO_PORT must be set"); 21 | let db_name = env::var("DB_NAME").expect("DB_NAME env var must be set"); 22 | let manager = MongodbConnectionManager::new( 23 | ConnectionOptions::builder() 24 | .with_host(&mongo_addr, mongo_port.parse::().unwrap()) 25 | .with_db(&db_name) 26 | //.with_auth("root", "password") 27 | .build(), 28 | ); 29 | match Pool::builder().max_size(64).build(manager) { 30 | Ok(pool) => pool, 31 | Err(e) => panic!("Error: failed to create mongodb pool {}", e), 32 | } 33 | } 34 | 35 | /* 36 | Create a implementation of FromRequest so Conn can be provided at every api endpoint 37 | */ 38 | impl<'a, 'r> FromRequest<'a, 'r> for Conn { 39 | type Error = (); 40 | 41 | fn from_request(request: &'a Request<'r>) -> request::Outcome { 42 | let pool = request.guard::>()?; 43 | match pool.get() { 44 | Ok(db) => Outcome::Success(Conn(db)), 45 | Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), 46 | } 47 | } 48 | } 49 | 50 | /* 51 | When Conn is dereferencd, return the mongo connection. 52 | */ 53 | impl Deref for Conn { 54 | type Target = PooledConnection; 55 | 56 | fn deref(&self) -> &Self::Target { 57 | &self.0 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/cats/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::cats; 2 | use crate::mongo_connection::Conn; 3 | use cats::Cat; 4 | use mongodb::{doc, error::Error, oid::ObjectId}; 5 | use rocket::http::Status; 6 | use rocket_contrib::json::Json; 7 | 8 | fn error_status(error: Error) -> Status { 9 | match error { 10 | Error::CursorNotFoundError => Status::NotFound, 11 | _ => Status::InternalServerError, 12 | } 13 | } 14 | 15 | #[get("/")] 16 | pub fn all(connection: Conn) -> Result>, Status> { 17 | match cats::repository::all(&connection) { 18 | Ok(res) => Ok(Json(res)), 19 | Err(err) => Err(error_status(err)), 20 | } 21 | } 22 | 23 | #[get("/")] 24 | pub fn get(id: String, connection: Conn) -> Result, Status> { 25 | match ObjectId::with_string(&String::from(&id)) { 26 | Ok(res) => match cats::repository::get(res, &connection) { 27 | Ok(res) => Ok(Json(res.unwrap())), 28 | Err(err) => Err(error_status(err)), 29 | }, 30 | Err(_) => Err(error_status(Error::DefaultError(String::from( 31 | "Couldn't parse ObjectId", 32 | )))), 33 | } 34 | } 35 | 36 | #[post("/", format = "application/json", data = "")] 37 | pub fn post(cats: Json, connection: Conn) -> Result, Status> { 38 | match cats::repository::insert(cats.into_inner(), &connection) { 39 | Ok(res) => Ok(Json(res)), 40 | Err(err) => Err(error_status(err)), 41 | } 42 | } 43 | 44 | #[put("/", format = "application/json", data = "")] 45 | pub fn put(id: String, cats: Json, connection: Conn) -> Result, Status> { 46 | match ObjectId::with_string(&String::from(&id)) { 47 | Ok(res) => match cats::repository::update(res, cats.into_inner(), &connection) { 48 | Ok(res) => Ok(Json(res)), 49 | Err(err) => Err(error_status(err)), 50 | }, 51 | Err(_) => Err(error_status(Error::DefaultError(String::from( 52 | "Couldn't parse ObjectId", 53 | )))), 54 | } 55 | } 56 | 57 | #[delete("/")] 58 | pub fn delete(id: String, connection: Conn) -> Result, Status> { 59 | match ObjectId::with_string(&String::from(&id)) { 60 | Ok(res) => match cats::repository::delete(res, &connection) { 61 | Ok(_) => Ok(Json(id)), 62 | Err(err) => Err(error_status(err)), 63 | }, 64 | Err(_) => Err(error_status(Error::DefaultError(String::from( 65 | "Couldn't parse ObjectId", 66 | )))), 67 | } 68 | } 69 | 70 | #[delete("/")] 71 | pub fn delete_all(connection: Conn) -> Result, Status> { 72 | match cats::repository::delete_all(&connection) { 73 | Ok(_) => Ok(Json(true)), 74 | Err(err) => Err(error_status(err)), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cats/repository.rs: -------------------------------------------------------------------------------- 1 | #![allow(proc_macro_derive_resolution_fallback)] 2 | use crate::cats::{Cat, InsertableCat}; 3 | use crate::mongo_connection::Conn; 4 | use crate::r2d2_mongodb::mongodb::db::ThreadedDatabase; 5 | use mongodb::{bson, coll::results::DeleteResult, doc, error::Error, oid::ObjectId}; 6 | 7 | const COLLECTION: &str = "cats"; 8 | 9 | pub fn all(connection: &Conn) -> Result, Error> { 10 | let cursor = connection.collection(COLLECTION).find(None, None).unwrap(); 11 | 12 | cursor 13 | .map(|result| match result { 14 | Ok(doc) => match bson::from_bson(bson::Bson::Document(doc)) { 15 | Ok(result_model) => Ok(result_model), 16 | Err(_) => Err(Error::DefaultError(String::from(""))), 17 | }, 18 | Err(err) => Err(err), 19 | }) 20 | .collect::, Error>>() 21 | } 22 | 23 | pub fn get(id: ObjectId, connection: &Conn) -> Result, Error> { 24 | match connection 25 | .collection(COLLECTION) 26 | .find_one(Some(doc! {"_id": id}), None) 27 | { 28 | Ok(db_result) => match db_result { 29 | Some(result_doc) => match bson::from_bson(bson::Bson::Document(result_doc)) { 30 | Ok(result_model) => Ok(Some(result_model)), 31 | Err(_) => Err(Error::DefaultError(String::from( 32 | "Failed to create reverse BSON", 33 | ))), 34 | }, 35 | None => Ok(None), 36 | }, 37 | Err(err) => Err(err), 38 | } 39 | } 40 | 41 | pub fn insert(cats: Cat, connection: &Conn) -> Result { 42 | let insertable = InsertableCat::from_cat(cats.clone()); 43 | match bson::to_bson(&insertable) { 44 | Ok(model_bson) => match model_bson { 45 | bson::Bson::Document(model_doc) => { 46 | match connection 47 | .collection(COLLECTION) 48 | .insert_one(model_doc, None) 49 | { 50 | Ok(res) => match res.inserted_id { 51 | Some(res) => match bson::from_bson(res) { 52 | Ok(res) => Ok(res), 53 | Err(_) => Err(Error::DefaultError(String::from("Failed to read BSON"))), 54 | }, 55 | None => Err(Error::DefaultError(String::from("None"))), 56 | }, 57 | Err(err) => Err(err), 58 | } 59 | } 60 | _ => Err(Error::DefaultError(String::from( 61 | "Failed to create Document", 62 | ))), 63 | }, 64 | Err(_) => Err(Error::DefaultError(String::from("Failed to create BSON"))), 65 | } 66 | } 67 | 68 | pub fn update(id: ObjectId, cats: Cat, connection: &Conn) -> Result { 69 | let mut new_cat = cats.clone(); 70 | new_cat.id = Some(id.clone()); 71 | match bson::to_bson(&new_cat) { 72 | Ok(model_bson) => match model_bson { 73 | bson::Bson::Document(model_doc) => { 74 | match connection.collection(COLLECTION).replace_one( 75 | doc! {"_id": id}, 76 | model_doc, 77 | None, 78 | ) { 79 | Ok(_) => Ok(new_cat), 80 | Err(err) => Err(err), 81 | } 82 | } 83 | _ => Err(Error::DefaultError(String::from( 84 | "Failed to create Document", 85 | ))), 86 | }, 87 | Err(_) => Err(Error::DefaultError(String::from("Failed to create BSON"))), 88 | } 89 | } 90 | 91 | pub fn delete(id: ObjectId, connection: &Conn) -> Result { 92 | connection 93 | .collection(COLLECTION) 94 | .delete_one(doc! {"_id": id}, None) 95 | } 96 | 97 | pub fn delete_all(connection: &Conn) -> Result<(), Error> { 98 | connection.collection(COLLECTION).drop() 99 | } 100 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | // See https://rocket.rs/v0.4/guide/testing/#local-dispatching 2 | #[cfg(test)] 3 | mod test { 4 | use rocket::http::{ContentType, Status}; 5 | use rocket::local::Client; 6 | use rustlang_rocket_mongodb::rocket; 7 | 8 | #[test] 9 | fn get_cats() { 10 | let client = Client::new(rocket()).expect("valid rocket instance"); 11 | let response = client.get("/cats").dispatch(); 12 | assert_eq!(response.status(), Status::Ok); 13 | } 14 | 15 | #[test] 16 | fn get_cat() { 17 | // Well get and post tests are identical ... 18 | let client = Client::new(rocket()).expect("valid rocket instance"); 19 | let mut response = client 20 | .post("/cats") 21 | .header(ContentType::JSON) 22 | .body(r#"{ "name": "chacha" }"#) 23 | .dispatch(); 24 | assert_eq!(response.status(), Status::Ok); 25 | 26 | let id = response.body_string().unwrap(); 27 | let id: Vec<&str> = id.split("\"").collect(); 28 | let mut response = client.get(format!("/cats/{}", id[3])).dispatch(); 29 | assert!(response.body().is_some()); 30 | assert!(response.body_string().unwrap().contains(&id[3])); 31 | client.delete("/cats").dispatch(); 32 | } 33 | 34 | #[test] 35 | fn post_cat() { 36 | let client = Client::new(rocket()).expect("valid rocket instance"); 37 | let mut response = client 38 | .post("/cats") 39 | .header(ContentType::JSON) 40 | .body(r#"{ "name": "chacha" }"#) 41 | .dispatch(); 42 | assert_eq!(response.status(), Status::Ok); 43 | 44 | let id = response.body_string().unwrap(); 45 | let id: Vec<&str> = id.split("\"").collect(); 46 | let mut response = client.get(format!("/cats/{}", id[3])).dispatch(); 47 | assert!(response.body().is_some()); 48 | assert!(response.body_string().unwrap().contains(&id[3])); 49 | client.delete("/cats").dispatch(); 50 | } 51 | 52 | #[test] 53 | fn update_cat() { 54 | let client = Client::new(rocket()).expect("valid rocket instance"); 55 | let mut response = client 56 | .post("/cats") 57 | .header(ContentType::JSON) 58 | .body(r#"{ "name": "chacha" }"#) 59 | .dispatch(); 60 | 61 | assert_eq!(response.status(), Status::Ok); 62 | assert!(response.body().is_some()); 63 | let id = response.body_string().unwrap(); 64 | let id: Vec<&str> = id.split("\"").collect(); 65 | let response = client 66 | .put(format!("/cats/{}", id[3])) 67 | .header(ContentType::JSON) 68 | .body(r#"{ "name": "chichi" }"#) 69 | .dispatch(); 70 | assert_eq!(response.status(), Status::Ok); 71 | let mut response = client.get(format!("/cats/{}", id[3])).dispatch(); 72 | assert_eq!(response.status(), Status::Ok); 73 | assert!(response.body().is_some()); 74 | assert!(response.body_string().unwrap().contains("chichi")); 75 | client.delete("/cats").dispatch(); 76 | } 77 | 78 | #[test] 79 | fn delete_cat() { 80 | let client = Client::new(rocket()).expect("valid rocket instance"); 81 | let mut response = client 82 | .post("/cats") 83 | .header(ContentType::JSON) 84 | .body(r#"{ "name": "chacha" }"#) 85 | .dispatch(); 86 | assert_eq!(response.status(), Status::Ok); 87 | 88 | let id = response.body_string().unwrap(); 89 | let id: Vec<&str> = id.split("\"").collect(); 90 | let mut response = client.delete(format!("/cats/{}", id[3])).dispatch(); 91 | assert!(response.body().is_some()); 92 | assert!(response.body_string().unwrap().contains(&id[3])); 93 | client.delete("/cats").dispatch(); 94 | } 95 | 96 | #[test] 97 | fn delete_all() { 98 | let client = Client::new(rocket()).expect("valid rocket instance"); 99 | client.delete("/cats").dispatch(); 100 | let response = client 101 | .post("/cats") 102 | .header(ContentType::JSON) 103 | .body(r#"{ "name": "chacha" }"#) 104 | .dispatch(); 105 | assert_eq!(response.status(), Status::Ok); 106 | let response = client.delete("/cats").dispatch(); 107 | assert_eq!(response.status(), Status::Ok); 108 | } 109 | } 110 | --------------------------------------------------------------------------------