35
loading...
This website collects cookies to deliver better user experience
post
of this serie, making the final refactor to our application and tiding the loose ends to complete the ci/cd
setup.but we are starting to have some duplicate code between the endpoints > for the rest api and the endpoint for some pages.
endpoints
, for each page, we dup some code to get the information from the database. We can resolve this decoupling the that logic from the endpoints and creating a new reusable layer.business
or domain
logic.src
and two controller files. One for the views ( or pages ) and the other one for the api
.mkdir controllers && cd controllers
touch {mod.rs,dino.rs,views.rs}
endpoint
function to each one and declare the modules in our mod
file.// dino.rs
pub async fn create(mut req: Request<State>) -> tide::Result {
todo!();
}
pub async fn list(req: tide::Request<State>) -> tide::Result {
todo!();
}
pub async fn get(req: tide::Request<State>) -> tide::Result {
todo!();
}
pub async fn update(mut req: tide::Request<State>) -> tide::Result {
todo!();
}
pub async fn delete(req: tide::Request<State>) -> tide::Result {
todo!();
}
endpoint
logic to the one used to interact to the database. So, this functions will receive the request and call the other layer ( let's call handler
) to make crud operations.handler
layer and expose the api to be used by the controllers
. So again inside our src
directory.mkdir handlers && cd handlers
touch {mod.rs,dino.rs}
// handlers/dino.rs
pub async fn create(dino: Dino, db_pool: PgPool) -> tide::Result<Dino> {
todo!();
}
pub async fn list(db_pool: PgPool) -> tide::Result<Vec<Dino>> {
todo!();
}
pub async fn get(id: Uuid, db_pool: PgPool) -> tide::Result<Option<Dino>> {
todo!();
}
pub async fn delete(id: Uuid, db_pool: PgPool) -> tide::Result<Option<()>> {
todo!();
}
pub async fn update(id: Uuid, dino: Dino, db_pool: PgPool) -> tide::Result<Option<Dino>> {
todo!();
}
todo!()
and implement the actual logic, here is an example of the create
operation but you can check the PR to see the full code.// controllers/dino.rs
use crate::handlers;
pub async fn create(mut req: Request<State>) -> tide::Result {
let dino: Dino = req.body_json().await?;
let db_pool = req.state().db_pool.clone();
let row = handlers::dino::create(dino, db_pool).await?;
let mut res = Response::new(201);
res.set_body(Body::from_json(&row)?);
Ok(res)
}
pub async fn create(dino: Dino, db_pool: PgPool) -> tide::Result<Dino> {
let row: Dino = query_as!(
Dino,
r#"
INSERT INTO dinos (id, name, weight, diet) VALUES
($1, $2, $3, $4) returning id, name, weight, diet
"#,
dino.id,
dino.name,
dino.weight,
dino.diet
)
.fetch_one(&db_pool)
.await
.map_err(|e| Error::new(409, e))?;
Ok(row)
}
// controllers/views.rs
pub async fn index(req: Request<State>) -> tide::Result {
let tera = req.state().tera.clone();
let db_pool = req.state().db_pool.clone();
let rows = handlers::dino::list(db_pool).await?;
tera.render_response(
"index.html",
&context! {
"title" => String::from("Tide basic CRUD"),
"dinos" => rows
},
)
}
pub async fn new(req: Request<State>) -> tide::Result {
let tera = req.state().tera.clone();
tera.render_response(
"form.html",
&context! {
"title" => String::from("Create new dino")
},
)
}
pub async fn edit(req: Request<State>) -> tide::Result {
let tera = req.state().tera.clone();
let db_pool = req.state().db_pool.clone();
let id: Uuid = Uuid::parse_str(req.param("id")?).unwrap();
let row = handlers::dino::get(id, db_pool).await?;
let res = match row {
None => Response::new(404),
Some(row) => {
let mut r = Response::new(200);
let b = tera.render_body(
"form.html",
&context! {
"title" => String::from("Edit dino"),
"dino" => row
},
)?;
r.set_body(b);
r
}
};
Ok(res)
}
handler
to retrieve the dinos
information. We are almost there, we need now to clean our main file and add tide the routes to the controllers. We can start by removing all the restEntity
logic in main and just tide the routes to the controller functions in our server fn
// views
app.at("/").get(views::index);
app.at("/dinos/new").get(views::new);
app.at("/dinos/:id/edit").get(views::edit);
// api
app.at("/dinos").get(dino::list).post(dino::create);
app.at("dinos/:id")
.get(dino::get)
.put(dino::update)
.delete(dino::delete);
app.at("/public")
.serve_dir("./public/")
.expect("Invalid static file directory");
app
tests
cargo test
running 10 tests
test tests::clear ... ok
test tests::delete_dino ... ok
test tests::create_dino_with_existing_key ... ok
test tests::delete_dino_non_existing_key ... ok
test tests::create_dino ... ok
test tests::get_dino ... ok
test tests::update_dino ... ok
test tests::get_dino_non_existing_key ... ok
test tests::list_dinos ... ok
test tests::updatet_dino_non_existing_key ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
imports
.cd
part for deploy our app in each commit to main
.sqlx
to enable offline
mode and allow us to build our app without connecting to the database.// Cargo.toml
sqlx = { version = "0.4.2", features = ["runtime-async-std-rustls", "offline", "macros", "chrono", "json", "postgres", "uuid"] }
offline
feature and change runtime-async-std
for one of the supported ones in the new version ( in our case runtime-async-std-rustls
).pub async fn make_db_pool(db_url: &str) -> PgPool {
Pool::connect(db_url).await.unwrap()
}
as "id!"
in the query for inserting a Dino to prevent the inferred field type (Option)
// use as "id!" here to prevent sqlx use the inferred ( nulleable ) type
// since we know that id should not be null
// see : https://github.com/launchbadge/sqlx/blob/master/src/macros.rs#L482
pub async fn create(dino: Dino, db_pool: &PgPool) -> tide::Result<Dino> {
let row: Dino = query_as!(
Dino,
r#"
INSERT INTO dinos (id, name, weight, diet) VALUES
($1, $2, $3, $4) returning id as "id!", name, weight, diet
"#,
dino.id,
dino.name,
dino.weight,
dino.diet
)
.fetch_one(db_pool)
.await
.map_err(|e| Error::new(409, e))?;
Ok(row)
}
sqlx-cli
and generate the needed file to build without need a db connection.cargo install sqlx-cli
cargo sqlx prepare
(...)
query data written to `sqlx-data.json` in the current directory; please check this into version control
The simplest way to deploy your full-stack apps
. Also, they have a community
plan so let's try and see how it's go.tide
.