31
loading...
This website collects cookies to deliver better user experience
Zero to Production in Rust by Luca Palmieri (@algo_luca)
Witter, a twitter clone in Rust by David Pedersen (@davidpdrsn )
binary
project.$ cargo new tide-basic-crud && cd tide-basic-crud
tide
as dependency since is the http framework
I selected to use, we can add it by hand editing the
Cargo.toml
file or install cargo-edit and use the add
command.$ cargo install cargo-edit
$ cargo add tide
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding tide v0.13.0 to dependencies
async-std
with the attributes
feature enable by adding this line to your deps ( in Cargo.toml
file ).async-std = { version = "1.6.0", features = ["attributes"] }
main.rs
with the basic example.#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
tide::log::start();
let mut app = tide::new();
app.at("/").get(|_| async { Ok("Hello, world!") });
app.listen("127.0.0.1:8080").await?;
Ok(())
}
$ cargo run
[Running 'cargo run']
Compiling tide-basic-crud v0.1.0 (/Users/pepo/personal/rust/tide-basic-crud)
Finished dev [unoptimized + debuginfo] target(s) in 5.25s
Running `target/debug/tide-basic-crud`
tide::log Logger started
level Info
tide::listener::tcp_listener Server listening on http://127.0.0.1:8080
curl
$ curl localhost:8080
Hello, world!
CRUD
that allow us to track dinosaurs
information.name
, weight
( in kilograms ) and the diet
( type ).dinos
, adding the /dinos
route ( at ) with the verb post
and following the request/response concept(...)
app.at("/dinos").post(|mut req: Request<()>| async move {
let body = req.body_string().await?;
println!("{:?}", body);
let mut res = Response::new(201);
res.set_body(String::from("created!"));
Ok(res)
});
$ curl -v -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos
(...)
* upload completely sent off: 59 out of 59 bytes
< HTTP/1.1 201 Created
(...)
created!
logs
tide::log::middleware <-- Request received
method POST
path /dinos
"{\"name\":\"velociraptor\", \"weight\": 50, \"diet\":\"carnivorous\"}"
tide::log::middleware --> Response sent
method POST
path /dinos
status 201
duration 59.609µs
as string
and we need to parse as json
. If you are familiarized with node.js
and express
this could be done with the body-parser
middleware, but tide
can parse json
and form
(urlencoded) out of the box with body_json
and body_form
methods.body_string()
to body_json
and try again.curl -v -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos
< HTTP/1.1 422 Unprocessable Entity
422 Unprocessable Entity
, doesn't works as expected (or maybe yes). Tide
use serde
to deserialize the request body and need to be parse into
a struct. So, let's create our Dino
struct and deserialize the body into.#[derive(Debug, Deserialize, Serialize)]
struct Dino {
name: String,
weight: u16,
diet: String
}
derive
attributes to all to serialize/deserialize
, now let's change the route to deserialize the body into the the Dino
struct and return the json
representation.app.at("/dinos").post(|mut req: Request<()>| async move {
let dino: Dino = req.body_json().await?;
println!("{:?}", dino);
let mut res = Response::new(201);
res.set_body(Body::from_json(&dino)?);
Ok(res)
});
$ curl -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos
{"name":"velociraptor","weight":50,"diet":"carnivorous"}
dinos
using a hashMap
to store a key/value
in memory. We will add a db persistence later.tide
documentation, we can use tide::
with_state to create a server with shared application scoped state.hashMap
.#[derive(Clone,Debug)]
struct State {
dinos: Arc<RwLock<HashMap<String,Dino>>>
}
wrap
our hashMap in a mutex here to use in the State
( thanks to the tide awesome community for the tip ).fn
let state = State {
dinos: Default::default()
};
let mut app = tide::with_state(state);
app.at("/").get(|_| async { Ok("Hello, world!") });
app.at("/dinos")
.post(|mut req: Request<State>| async move {
let dino: Dino = req.body_json().await?;
// let get a mut ref of our store ( hashMap )
let mut dinos = req.state().dinos.write().await;
dinos.insert(String::from(&dino.name), dino.clone());
let mut res = Response::new(201);
res.set_body(Body::from_json(&dino)?);
Ok(res)
})
list
the dinos
and check how it's worksapp.at("/dinos")
.get(|req: Request<State>| async move {
let dinos = req.state().dinos.read().await;
// get all the dinos as a vector
let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
let mut res = Response::new(200);
res.set_body(Body::from_json(&dinos_vec)?);
Ok(res)
})
$ curl -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos
$ curl -d '{"name":"t-rex", "weight": 5000, "diet":"carnivorous"}' http://localhost:8080/dinos
$ curl http://localhost:8080/dinos
[{"name":"velociraptor","weight":50,"diet":"carnivorous"},{"name":"t-rex","weight":5000,"diet":"carnivorous"}]
dino
...app.at("/dinos/:name")
.get(|req: Request<State>| async move {
let mut dinos = req.state().dinos.write().await;
let key: String = req.param("name")?;
let res = match dinos.entry(key) {
Entry::Vacant(_entry) => Response::new(404),
Entry::Occupied(entry) => {
let mut res = Response::new(200);
res.set_body(Body::from_json(&entry.get())?);
res
}
};
Ok(res)
})
entry
api so, before using here we need to bring it to the scope. We can do it adding this line at the top of the fileuse std::collections::hash_map::Entry;
match
in the get to return the dino
or 404
if the requested name doesn't exists.$ curl http://localhost:8080/dinos/t-rex
{"name":"t-rex","weight":5000,"diet":"carnivorous"}
$ curl -I http://localhost:8080/dinos/trex
HTTP/1.1 404 Not Found
content-length: 0
CRUD
..put(|mut req: Request<State>| async move {
let dino_update: Dino = req.body_json().await?;
let mut dinos = req.state().dinos.write().await;
let key: String = req.param("name")?;
let res = match dinos.entry(key) {
Entry::Vacant(_entry) => Response::new(404),
Entry::Occupied(mut entry) => {
*entry.get_mut() = dino_update;
let mut res = Response::new(200);
res.set_body(Body::from_json(&entry.get())?);
res
}
};
Ok(res)
})
.delete(|req: Request<State>| async move {
let mut dinos = req.state().dinos.write().await;
let key: String = req.param("name")?;
let deleted = dinos.remove(&key);
let res = match deleted {
None => Response::new(404),
Some(_) => Response::new(204),
};
Ok(res)
});
$ curl -v -X PUT -d '{"name":"t-rex", "weight": 5, "diet":"carnivorous"}' http://localhost:8080/dinos/t-rex
$ curl http://localhost:8080/dinos/t-rex
{"name":"t-rex","weight":5,"diet":"carnivorous"}
$ curl -v -X DELETE http://localhost:8080/dinos/t-rex
$ curl -I http://localhost:8080/dinos/t-rex
HTTP/1.1 404 Not Found
lot
of manual testing, let's add some basic unit test to smooth the next steps.main
function of the server
creation allowing us to create a server without need to actually listen in any port.#[async_std::main]
async fn main() {
tide::log::start();
let dinos_store = Default::default();
let app = server(dinos_store).await;
app.listen("127.0.0.1:8080").await.unwrap();
}
async fn server(dinos_store: Arc<RwLock<HashMap<String, Dino>>>) -> Server<State> {
let state = State {
dinos: dinos_store, //Default::default(),
};
let mut app = tide::with_state(state);
app.at("/").get(|_| async { Ok("ok") });
(...)
app
server
function using cargo
for running our tests. There is more information about tests in the cargo book but in generalCargo can run your tests with the cargo test command. Cargo looks for tests to run in two places: in each of your src files and any tests in tests/. Tests in your src files should be unit tests, and tests in tests/ should be integration-style tests. As such, you’ll need to import your crates into the files in tests.
#[async_std::test]
async fn list_dinos() -> tide::Result<()> {
use tide::http::{Method, Request, Response, Url};
let dino = Dino {
name: String::from("test"),
weight: 50,
diet: String::from("carnivorous"),
};
let mut dinos_store = HashMap::new();
dinos_store.insert(dino.name.clone(), dino);
let dinos: Vec<Dino> = dinos_store.values().cloned().collect();
let dinos_as_json_string = serde_json::to_string(&dinos)?;
let state = Arc::new(RwLock::new(dinos_store));
let app = server(state).await;
let url = Url::parse("https://example.com/dinos").unwrap();
let req = Request::new(Method::Get, url);
let mut res: Response = app.respond(req).await?;
let v = res.body_string().await?;
assert_eq!(dinos_as_json_string, v);
Ok(())
}
#[async_std::test]
async fn create_dino() -> tide::Result<()> {
use tide::http::{Method, Request, Response, Url};
let dino = Dino {
name: String::from("test"),
weight: 50,
diet: String::from("carnivorous"),
};
let dinos_store = HashMap::new();
let state = Arc::new(RwLock::new(dinos_store));
let app = server(state).await;
let url = Url::parse("https://example.com/dinos").unwrap();
let mut req = Request::new(Method::Post, url);
req.set_body(serde_json::to_string(&dino)?);
let res: Response = app.respond(req).await?;
assert_eq!(201, res.status());
Ok(())
}
tide-users
discord channel.app
), calling the endpoint
with a req
uest without need to make an actual http request
and have the server listen
in any port.cargo test
$ cargo test
Compiling tide-basic-crud v0.1.0 (/Users/pepo/personal/rust/tide-basic-crud)
Finished test [unoptimized + debuginfo] target(s) in 5.99s
Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5
running 5 tests
test list_dinos ... ok
test index_page ... ok
test create_dino ... ok
test delete_dino ... ok
test update_dino ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
cargo
is an awesome tool and you can add more functionality like watching
changes in you code and react ( for example running test ) with cargo-watch.crud
with tide
and any feedback is welcome :) .dinos_store
to a database ( postgresql / sqlx ).