Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to use modern Rust dependencies #4

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[package]

name = "rust-users"
version = "0.0.1"
version = "0.1.0"
authors = [ "Ryan Chenkie <[email protected]>" ]
edition = "2021"

[dependencies]
nickel = "*"
mongodb = "*"
bson = "*"
rustc-serialize = "*"
hyper = "*"
jwt = "*"
rust-crypto = "*"
actix-web = "4.0"
actix-service = "2.0"
env_logger = "0.10"
mongodb = "2.3"
serde = { version = "1.0", features = ["derive"] }
jsonwebtoken = "8.1"
futures-util = "0.3"
futures = "0.3.30"
192 changes: 114 additions & 78 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,76 @@
# Rust API Example

This repo shows how to implement a RESTful API in Rust with **[Nickel.rs](http://nickel.rs/)** and the **[MongoDB Rust Driver](https://github.com/mongodb-labs/mongo-rust-driver-prototype)**.
This repo shows how to implement a RESTful API in Rust with **[Actix Web](https://actix.rs/)** and the **[MongoDB Rust Driver](https://www.mongodb.com/docs/drivers/rust/current/)**.

This REST API uses simple JWT authentication. Note that it does not actually integrate with Auth0!

## Prerequisites

You will need to have MongoDB installed and running on `localhost:27017`. If on macOS, for instance, follow MongoDB's [installation instructions](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/).

## Usage

Issue a POST to the `/login` route to obtain a JWT bearer token.

```
% curl -v http://127.0.0.1:9000/login -X POST -d '{"email": "Joe User", "password": "secret"}' -H "Content-Type: application/json"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:9000...
* Connected to 127.0.0.1 (127.0.0.1) port 9000
> POST /login HTTP/1.1
> Host: 127.0.0.1:9000
> User-Agent: curl/8.4.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 43
>
< HTTP/1.1 200 OK
< content-length: 180
< date: Wed, 10 Jul 2024 14:20:55 GMT
<
* Connection #0 to host 127.0.0.1 left intact
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E
```

The JWT bearer token can now be specified to the protected routes such as `/users`.

```
% curl -v -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E" http://127.0.0.1:9000/users -X POST -d '{"firstname": "Joe", "lastname": "User", "email": "[email protected]"}' -H "Content-Type: application/json"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:9000...
* Connected to 127.0.0.1 (127.0.0.1) port 9000
> POST /users HTTP/1.1
> Host: 127.0.0.1:9000
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E
> Content-Type: application/json
> Content-Length: 68
>
< HTTP/1.1 200 OK
< content-length: 11
< date: Wed, 10 Jul 2024 14:23:07 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Item saved!

% curl -v -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E" http://127.0.0.1:9000/users
* Trying 127.0.0.1:9000...
* Connected to 127.0.0.1 (127.0.0.1) port 9000
> GET /users HTTP/1.1
> Host: 127.0.0.1:9000
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E
>
< HTTP/1.1 200 OK
< content-length: 127
< content-type: application/json
< date: Wed, 10 Jul 2024 14:23:51 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"data":[{ "_id": ObjectId("668e994c7eba267f28496f8a"), "firstname": "Joe", "lastname": "User", "email": "[email protected]" },]}
```

## Important Snippets

Expand All @@ -13,98 +83,73 @@ The **GET** `/users` route searches MongoDB for all users and then returns a JSO

...

fn get_data_string(result: MongoResult<Document>) -> Result<Json, String> {
async fn get_data_string(result: mongodb::error::Result<Document>) -> Result<web::Json<Document>, String> {
match result {
Ok(doc) => Ok(Bson::Document(doc).to_json()),
Err(e) => Err(format!("{}", e))
Ok(doc) => Ok(web::Json(doc)),
Err(e) => Err(format!("{}", e)),
}
}

router.get("/users", middleware! { |request, response|
async fn get_users() -> impl Responder {
let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap();
let client = Client::with_options(client_options).unwrap();

// Connect to the database
let client = Client::connect("localhost", 27017)
.ok().expect("Error establishing connection.");
let collection = client.database("rust-users").collection::<Document>("users");
let mut cursor = collection.find(None, None).await.unwrap();

// The users collection
let coll = client.db("rust-users").collection("users");

// Create cursor that finds all documents
let cursor = coll.find(None, None).unwrap();

// Opening for the JSON string to be returned
let mut data_result = "{\"data\":[".to_owned();

for (i, result) in cursor.enumerate() {
match get_data_string(result) {
while let Some(result) = cursor.try_next().await.unwrap() {
match get_data_string(Ok(result)).await {
Ok(data) => {
let string_data = if i == 0 {
format!("{}", data)
} else {
format!("{},", data)
};

let string_data = format!("{},", data.into_inner());
data_result.push_str(&string_data);
},

Err(e) => return response.send(format!("{}", e))
}
Err(e) => return HttpResponse::InternalServerError().body(e),
}
}

// Close the JSON string
data_result.push_str("]}");

// Send back the result
format!("{}", data_result)

});
HttpResponse::Ok()
.content_type("application/json")
.body(data_result)
}

...
```

The **POST** `/users/new` route takes JSON data and saves in the database. The data conforms to the `User` struct.
The **POST** `/users` route takes JSON data and saves in the database. The data conforms to the `User` struct.

```rust
// src/main.rs

...

#[derive(RustcDecodable, RustcEncodable)]
#[derive(Serialize, Deserialize)]
struct User {
firstname: String,
lastname: String,
email: String
email: String,
}

...

router.post("/users/new", middleware! { |request, response|
async fn new_user(info: web::Json<User>) -> impl Responder {
let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap();
let client = Client::with_options(client_options).unwrap();

// Accept a JSON string that corresponds to the User struct
let user = request.json_as::<User>().unwrap();

let firstname = user.firstname.to_string();
let lastname = user.lastname.to_string();
let email = user.email.to_string();

// Connect to the database
let client = Client::connect("localhost", 27017)
.ok().expect("Error establishing connection.");

// The users collection
let coll = client.db("rust-users").collection("users");
let collection = client.database("rust-users").collection::<Document>("users");
let user = doc! {
"firstname": &info.firstname,
"lastname": &info.lastname,
"email": &info.email,
};

// Insert one user
match coll.insert_one(doc! {
"firstname" => firstname,
"lastname" => lastname,
"email" => email
}, None) {
Ok(_) => (StatusCode::Ok, "Item saved!"),
Err(e) => return response.send(format!("{}", e))
match collection.insert_one(user, None).await {
Ok(_) => HttpResponse::Ok().body("Item saved!"),
Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)),
}

});
}

...
```
Expand All @@ -116,29 +161,20 @@ The **DELETE** `/users/:user_id` takes an `objectid` as a parameter, decodes it

...

router.delete("/users/:user_id", middleware! { |request, response|
async fn delete_user(req: HttpRequest) -> impl Responder {
let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap();
let client = Client::with_options(client_options).unwrap();

let client = Client::connect("localhost", 27017)
.ok().expect("Failed to initialize standalone client.");
let collection = client.database("rust-users").collection::<Document>("users");
let object_id = req.match_info().get("id").unwrap();

// The users collection
let coll = client.db("rust-users").collection("users");
let id = ObjectId::parse_str(object_id).unwrap();

// Get the user_id from the request params
let user_id = request.param("user_id").unwrap();

// Match the user id to an bson ObjectId
let id = match ObjectId::with_string(user_id) {
Ok(oid) => oid,
Err(e) => return response.send(format!("{}", e))
};

match coll.delete_one(doc! {"_id" => id}, None) {
Ok(_) => (StatusCode::Ok, "Item deleted!"),
Err(e) => return response.send(format!("{}", e))
match collection.delete_one(doc! {"_id": id}, None).await {
Ok(_) => HttpResponse::Ok().body("Item deleted!"),
Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)),
}

});
}

...
```
Expand Down
Loading