Film Repository
Today, our API will work with a Postgres database. But this may change in the future.
Even if that never happens (which is the most probable thing) we will still want to decouple our API from the database to make it easier to test and maintain.
To do that, we will leverage traits to define the behavior of our film repository.
This will also allow us to take a look at:
Defining the FilmRepository
trait
We will define this trait in the api-lib
crate although it could be its own crate if we wanted.
To keep it simple create a new film_repository
folder in api > lib > src
and add a mod.rs
file with the following content:
#![allow(unused)] fn main() { pub type FilmError = String; pub type FilmResult<T> = Result<T, FilmError>; pub trait FilmRepository: Send + Sync + 'static { async fn get_films(&self) -> FilmResult<Vec<Film>>; async fn get_film(&self, id: &Uuid) -> FilmResult<Film>; async fn create_film(&self, id: &CreateFilm) -> FilmResult<Film>; async fn update_film(&self, id: &Film) -> FilmResult<Film>; async fn delete_film(&self, id: &Uuid) -> FilmResult<Uuid>; } }
Don't forget to add the module to the lib.rs
file:
#![allow(unused)] fn main() { pub mod film_repository; }
The code won't compile. But don't worry, we will fix that in a minute.
Let's review for a moment that piece of code:
- We define two type aliases:
FilmError
andFilmResult<T>
. This will allow us to easily change theerror
type if we need to and to avoid boilerplate when having to write the return of our functions. - The Send & Sync traits will allow us to share and send this the types implementiong this trait between threads.
- The
'static
lifetime will make our life easier as we know that the repository will live for the entire duration of the program. - Finally, you see that we have defined 5 functions that will allow us to interact with our database. We will implement them in the next section.
Then, why does this code not compile?
The reason is that we are using the async
keyword in our trait definition. This is not allowed by the Rust compiler.
To fix this, we will use the async-trait crate.
async-trait
Let's bring this dependency into our api-lib
crate by adding it to the Cargo.toml
file. As we will be using the uuid
crate in our repository, we will also add it to the Cargo.toml
file:
[dependencies]
+ # utils
+ async-trait = "0.1.82"
+ uuid = { version = "1.3.4", features = ["serde", "v4", "js"] }
Now, let's mark our trait as async
and add all the use
statements we need:
+ use shared::models::{CreateFilm, Film};
+ use uuid::Uuid;
pub type FilmError = String;
pub type FilmResult<T> = Result<T, FilmError>;
+ #[async_trait::async_trait]
pub trait FilmRepository: Send + Sync + 'static {
async fn get_films(&self) -> FilmResult<Vec<Film>>;
async fn get_film(&self, id: &Uuid) -> FilmResult<Film>;
async fn create_film(&self, id: &CreateFilm) -> FilmResult<Film>;
async fn update_film(&self, id: &Film) -> FilmResult<Film>;
async fn delete_film(&self, id: &Uuid) -> FilmResult<Uuid>;
}
Now, the code compiles. But we still need to implement the trait. We will do it in the next section.
mod.rs
You probably noticed that we created file called mod.rs
in the film_repository
folder.
So far, whenever we wanted to create a new module, we just used a file with the same name as the module. For example, we created a film
module by creating a film.rs
file.
There are several ways to work with modules, you can learn more about it here.
This is the old way of doing things with modules but it's still valid and widely used in the Rust community.
Most of the time, you will do this if you plan to add more modules under the film_repository
folder. For example, you could add a memory_film_repository
module to implement a memory repository.
For now, let's commit our changes:
git add .
git commit -m "add film repository trait"