Build Basic🦀Rust CLI Todo App Using Clap and SeaOrm
Tags: rust, cli, postgres, tutorial
Published: 2023-11-17 15:02:38

## Introduction
It has been enjoyable using Rust to build a CLI app, working with both Clap as the CLI crate and SeaOrm as the ORM. In this hands-on tutorial, we'll walk through the process of creating a fully functional Todo application using Rust. This guide stems from my concern in lack of Rust tutorials that using Clap and SeaOrm altogether. If you're just as newbie as me and looking for light guide to become a Crab Master 🦀, you're in the right place!

## Here's the preview
{% embed https://youtu.be/CHe4_tTYlDE %}

## Prerequisites
- Postgres (my PostgreSql version is 16.0)
- Rust 1.73.0
- Cargo

## Step 1: Setting up the Project

```
Cargo new pagawean
cd pagawean
```
I name the project as pagawean means tasks in sundanese. It's a funny thing to me to name it, so name the project as you like because we will call it in our own cli as the command.

Add required crates just like below.
This is inside the `Cargo.toml` file until this step:
```toml
[package]
name = "pagawean"
version = "0.1.0"
edition = "2021"

[dependencies]
chrono = "0.4.31"
clap = { version = "4.4.6", features = ["derive"] }
serde = "1.0.188"
serde_json = "1.0.107"
uuid = { version = "1.4.1", features = ["v4", "serde"]}
sea-orm = { version = "0.12", features = [ "sqlx-mysql", "runtime-tokio-rustls", "macros" ] }
dotenvy_macro = "0.15.7"
tokio = { version = "1.33.0", features = ["full"] }
```

## Step 2: Installing sea-orm and Migrating
Run this command:
```
cargo install sea-orm-cli
```

After installing sea-orm-cli we could use that command to do migration. The step we would take is to migrate and then using that migration table scheme to generate an entity later on.

```
sea-orm-cli migrate init
sea-orm-cli migrate generate "create_todo"
sea-orm-cli migrate generate "create_user"
```

Once it's completed, SeaOrm will generate a module called `migration`in the root project. Inside the `src `directory, there would be some files that at the end of its name is `mblablabla_blabla_create_todo.rs`and `mblablabla_blabla_create_user.rs` as a result from the command we'd done previously. FYI, the convention is `mYYYYMMDD_HHMMSS_migration_name.rs` from the generated file. If you see a file `m20220101_000001_create_table.rs` it comes from the `sea-orm-cli migrate init`. Don't worry just ignore it. You will get the template migration code inside it but we need to change it the way we wanted. We'll get into it later.

This is the project structure should look like:

```
.
├── migration
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       ├── mblablabla_blabla_create_user.rs
│       ├── mblablabla_blabla_create_todo.rs
│       └── main.rs
└── src
    └── main.rs
├── Cargo.lock
├── Cargo.toml
```

Inside the `migration/Cargo.toml`, uncomment inside the features `[dependencies.sea-orm-migration]` like so.

File: `migration/Cargo.toml`

```toml
[dependencies.sea-orm-migration]
version = "0.12.0"
features = [
  # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
  # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
  # e.g.
  "runtime-tokio-rustls",  # `ASYNC_RUNTIME` feature
  "sqlx-postgres",         # `DATABASE_DRIVER` feature
]
```

In this case, the database we will create just a basic entity which is user and todo. User can have many todo, but each todo can only have one that belongs to user. For a much more clarity, look at this diagram: 

![Database Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18vpk1qa64kt329bo6m0.png)

Open the file, then edit inside `mblablabla_blabla_create_user.rs` based on the diagram. Don't forget to remove `todo!()` and save it.

File: `migration/src/mblablabla_blabla_create_user.rs`

```rust
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {

        manager
            .create_table(
                Table::create()
                    .table(User::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(User::Id)
                            .string()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(User::Name).string().not_null())
                    .col(ColumnDef::new(User::Email).string().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {

        manager
            .drop_table(Table::drop().table(User::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
pub enum User {
    Table,
    Id,
    Name,
    Email,
}
```
Change inside the `mblablabla_blabla_create_todo.rs` also like this:

File: `migration/src/mblablabla_blabla_create_todo.rs`

```rust
use sea_orm_migration::prelude::*;
use super::m20231102_030846_create_user::User;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {

        manager
            .create_table(
                Table::create()
                    .table(Todo::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Todo::Id)
                            .string()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Todo::Title).string().not_null())
                    .col(ColumnDef::new(Todo::Task).string().not_null())
                    .col(ColumnDef::new(Todo::CreatedAt).date_time().not_null())
                    .col(ColumnDef::new(Todo::DueDate).date_time().not_null())
                    .col(ColumnDef::new(Todo::UserId).string().not_null())
                    .foreign_key(ForeignKey::create()
                        .name("fk_todo_user_id")
                        .from_col(Todo::UserId)
                        .to_tbl(User::Table)
                        .to_col(User::Id)
                        .on_delete(ForeignKeyAction::Cascade)
                        .on_update(ForeignKeyAction::Cascade))
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {

        manager
            .drop_table(Table::drop().table(Todo::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
enum Todo {
    Table,
    Id,
    Title,
    Task,
    CreatedAt,
    DueDate,
    UserId
}
```

Create a `.env` to set URL for the database environment variable:

```
DATABASE_URL=protocol://username:password@host/database
```
Based on seaorm docs, this will guide you:
> protocol can be mysql:, postgres: or sqlite:. host is usually localhost, a domain name or an IP address.

read this page that linked to the docs [database connection](https://www.sea-ql.org/SeaORM/docs/install-and-config/connection/) 

Then, run the migration

```
sea-orm-cli migrate up
```
SeaOrm will apply the migrations based on the schema we did earlier.

## Step 3: Generating Entities
First we need to create a lib module that's called entity
```
cargo new entity --lib
```
Then, we generate entities based on the migration we did previously using `sea-orm-cli` with the output inside the entity lib module before. The SeaOrm will use the database connection from `DATABASE_URL` environment variable.
```
sea-orm-cli generate entity -o entity/src
```
Then, add sea-orm dependency into it
File: `entity/Cargo.toml`
```toml
[dependencies]
sea-orm = { version = "0.12" }
```
You should see that we have a generated entity in entity module

File: entity/src/todo.rs
```rust
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "todo")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: String,
    pub title: String,
    pub task: String,
    pub created_at: DateTime,
    pub due_date: DateTime,
    pub user_id: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::user::Entity",
        from = "Column::UserId",
        to = "super::user::Column::Id",
        on_update = "Cascade",
        on_delete = "Cascade"
    )]
    User,
}

impl Related<super::user::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}
```

This is for the user entity
File: entity/src/user.rs
```rust
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: String,
    pub name: String,
    pub email: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(has_many = "super::todo::Entity")]
    Todo,
}

impl Related<super::todo::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Todo.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}
```

**Rename** the `mod.rs` to `lib.rs` inside the `entity/src`

Now in order all the newly modules we created can work together and not getting error squiggling, we need to add `entity` and `migration` to the workspace and dependencies in our root project
File: Cargo.toml
```toml
[workspace]
members = [".", "migration", "entity"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" }
```
So now your whole `toml` file should look like this
File: Cargo.toml
```toml
[package]
name = "pagawean"
version = "0.1.0"
edition = "2021"

[workspace]
members = [".", "migration", "entity"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" }
chrono = "0.4.31"
clap = { version = "4.4.6", features = ["derive"] }
serde = "1.0.188"
serde_json = "1.0.107"
uuid = { version = "1.4.1", features = ["v4", "serde"]}
sea-orm = { version = "0.12", features = [ "sqlx-mysql", "runtime-tokio-rustls", "macros" ] }
dotenvy_macro = "0.15.7"
tokio = { version = "1.33.0", features = ["full"] }
```

> little tip: Refresh your text editor if your error squiggle still appearing. DO THIS ESPECIALLY IF YOU'RE USING VS CODE (rust analyzer)

So now this is your project structure until this step
```
.
├── entity
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       ├── prelude.rs
│       ├── todo.rs
│       └── user.rs
├── migration
│   ├── Cargo.toml
│   ├── Cargo.lock
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       ├── mblablabla_blabla_create_user.rs
│       ├── mblablabla_blabla_create_todo.rs
│       └── main.rs
└── src
    └── main.rs
├── Cargo.lock
├── Cargo.toml
```

## Step 4: Coding with Clap
For starters, we gotta need to breakdown what we want to build. We can create 2 possible inputs from our user that they want to create  which is an account and a todo list. To get a better view, take a look at this diagram for the main concept:

![Main concept](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b70r8lt8uj3v7cxkg331.png)

We're gonna create an args take take an input from the user. For the detail command args that we're gonna build you can look at this diagram:
![Pagawean Args](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yg6fx7os7lhvswps4hay.png)

Now create an `args` file inside the `src` root project. And write code like below:
File: src/args.rs

```rust
use clap::{Args, Parser, Subcommand};

#[derive(Parser, Debug)]
#[clap(author, version, about)]
pub struct PagaweanArgs {
    #[clap(subcommand)]
    pub entity_type: EntityType,
}

#[derive(Debug, Subcommand)]
pub enum EntityType {
    /// Create, Update, Delete, Read user
    User(UserCommand),

    /// Create, Update, Delete, Read todos
    Todo(TodoCommand),
}

#[derive(Debug, Args)]
pub struct UserCommand {
    #[clap(subcommand)]
    pub command: UserSubCommand,
}

#[derive(Debug, Subcommand)]
pub enum UserSubCommand {
    /// Create a new user
    Create(CreateUser),

    /// Update an existing user
    Update(UpdateUser),

    /// Delete an existing user
    Delete(DeleteEntity),

    /// Show all users
    Show,
}


#[derive(Debug, Args)]
pub struct CreateUser {
    /// name of the user
    pub name: String,

    /// email of the user
    pub email: String,
}
#[derive(Debug, Args)]
pub struct UpdateUser {
    /// id of the user
    pub id: String,

    /// name of the user
    pub name: String,

    /// email of the user
    pub email: String,
}

#[derive(Debug, Args)]
pub struct DeleteEntity {
    /// id of the user
    pub id: String,
}

#[derive(Debug, Args)]
pub struct TodoCommand {
    #[clap(subcommand)]
    pub command: TodoSubCommand,
}

#[derive(Debug, Subcommand)]
pub enum TodoSubCommand {
    /// Create a new todo
    Create(CreateTodo),

    /// Update an existing todo
    Update(UpdateTodo),

    /// Delete an existing todo
    Delete(DeleteTodo),

    /// Show all todos
    Read,

    /// Show all todos by user id
    ReadByUserId(ReadTodoByUserId),
}

#[derive(Debug, Args)]
pub struct CreateTodo {
    /// title of the todo
    pub title: String,

    /// task of the todo
    pub task: String,

    /// due date of the todo
    pub duedate: String,

    /// user id of the todo
    pub user_id: String,
}

#[derive(Debug, Args)]
pub struct UpdateTodo {
    /// id of the todo
    pub id: String,

    /// title of the todo
    pub title: String,

    /// task of the todo
    pub task: String,

    /// due date of the todo
    pub duedate: String,

    /// user id of the todo
    pub user_id: String
}

#[derive(Debug, Args)]
pub struct DeleteTodo {
    /// id of the todo
    pub id: String,
}

#[derive(Debug, Args)]
pub struct ReadTodoByUserId {
    /// user id of the todo
    pub user_id: String,
}
```
Here's what you gotta understand is that, the `derive` features is used heavily in this code. So what I could tell you is that `#[clap(subcommnad)]` tells you the field underneath it represents a subcommand when parsing command-line arguments.

Take a look at this snippet code that we wrote before:
File: src/args.rs
```rust
#[derive(Parser, Debug)]
#[clap(author, version, about)]
pub struct PagaweanArgs {
    #[clap(subcommand)]
    pub entity_type: EntityType,
}
```
By using this attribute, you're telling clap that the value provided for `entity_type` should correspond to one of the subcommands defined in the `EntityType` enum.

In this case, `clap` will recognize that `entity_type` is a subcommand field and expect the user to provide a subcommand, such as `user` or `todo`. Which is:
File: src/args.rs
```rust
#[derive(Debug, Subcommand)]
pub enum EntityType {
    /// Create, Update, Delete, Read user
    User(UserCommand),

    /// Create, Update, Delete, Read todos
    Todo(TodoCommand),
}
```
By defining a struct for this purpose, you are providing a clear and structured way for users to input the details of a new todo item from the command line.

File: src/args.rs
```rust
#[derive(Debug, Args)]
pub struct CreateTodo {
    pub title: String,
    pub task: String,
    pub duedate: String,
    pub user_id: String,
}
```
From the code above, when your program is executed with the appropriate command and arguments, clap will use the structure of this `CreateTodo` struct to parse and validate the provided command-line inputs. This applies to other structs we wrote earlier.

## Step 5: Integrating with SeaOrm
First we're gonna create a `db.rs` that will establish a connection to our local database, so that when we create a handler, the `db` module will connect to our local database.

Create a new file called `db.rs`, then write this code:
File: src/db.rs
```rust
use std::time::Duration;

use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr, DbConn};

pub async fn establish_connection(database_uri: &str) -> Result<DbConn, DbErr>{
    let mut opt = ConnectOptions::new(database_uri);
    opt.max_connections(100)
        .min_connections(5)
        .idle_timeout(Duration::from_secs(8));

    let database: Result<DatabaseConnection, DbErr> = Database::connect(opt).await;

    match database {
        Ok(db) => {
            println!("Connected to database");
            Ok(db.into())
        },
        Err(err) => {
            println!("Failed to connect to database");
            Err(err)
        }
    }

}
```
Next We're gonna create an ops as a handler that operate every time a certain command is called. 

First let's create a new directory called `ops`, inside that directory create files called `todo_ops.rs` and `user_ops.rs`. In order rust knows that there is a new module called `ops`, we're gonna need to create `mod.rs`. And inside that `mod.rs` write this code:
File: src/ops/mod.rs
```rust
pub mod todo_ops;
pub mod user_ops;
```
You might still get a error squiggle, so add this code inside the `src/main.rs` on the top of the file.
File: src/main.rs
```rust
mod ops;
```

### Write `user_ops` handler
In our `user_ops.rs`, it will handle the `UserCommand` and it will have a function that acts as an entry point that will responsible for a match enum based on the `UserCommand` input whether it's CREATE or UPDATE or DELETE or SHOW. 

File: src/ops/user_ops.rs
```rust
use dotenvy_macro::dotenv;
use sea_orm::{EntityTrait, Set, ActiveModelTrait, DeleteResult, DbConn, DbErr};
use uuid::Uuid;

use crate::db::{self, establish_connection};
use crate::args::{CreateUser, UpdateUser, DeleteEntity, UserSubCommand, UserCommand};

pub async fn handle_user_command(user: UserCommand) {
    let command = user.command;

    match command {
        UserSubCommand::Create(user) => {
            create_user(user).await;
        },
        UserSubCommand::Update(user) => {
            update_user(user).await;
        }
        UserSubCommand::Delete(user) => {
            delete_user(user).await;
        }
        UserSubCommand::Show => {
            show_users().await;
        }
    }
}

async fn create_user(user: CreateUser) {
    println!("Creating user: {:?}", user);

    
    let database_uri = dotenv!("DATABASE_URL");
    let db = db::establish_connection(database_uri).await;

    let id = Uuid::new_v4();

    let new_user = entity::user::ActiveModel {
        id: Set(id.to_string()),
        name: Set(user.name),
        email: Set(user.email),
        ..Default::default()
    };

    match db {
        Ok(db) => {
            let user = new_user.insert(&db).await.unwrap();
            println!("User created: {:?}", user);
        },
        Err(err) => {
            eprint!("Failed to establish a database connection and create user: {}", err)
        }
    }

}

async fn update_user(user: UpdateUser) {
    println!("Updating user: {:?}", user);

    let database_uri = dotenv!("DATABASE_URL");
    let db = db::establish_connection(database_uri).await;

    let id = Uuid::parse_str(&user.id).unwrap();

    type UserModel = entity::user::Model;

    match db {
        Ok(db) => {
            let find_user: Option<UserModel> = entity::user::Entity::find_by_id(id.clone())
                .one(&db)
                .await
                .unwrap();
            let user_active_model : entity::user::ActiveModel = find_user.unwrap().into();

            let updated_user = entity::user::ActiveModel {
                id: Set(id.to_string()),
                name: Set(user.name),
                email: Set(user.email),
                ..user_active_model
            };

            let updated_user: entity::user::Model = updated_user.update(&db).await.unwrap();

            println!("User updated: {:?}", updated_user);
        },

        Err(err) => {
            eprint!("Failed to establish a database connection and update user: {}", err)
        }
    }

}

async fn delete_user(user: DeleteEntity) {
    println!("Deleting user: {:?}", user);

    let database_uri = dotenv!("DATABASE_URL");
    let db = establish_connection(database_uri).await;

    let id = Uuid::parse_str(&user.id).unwrap();

    match db {
        Ok(db) => {
            let res: DeleteResult = entity::user::Entity::delete_by_id(id.clone())
                .exec(&db)
                .await
                .unwrap();

            println!("User deleted: {:?}", res);
        }
        Err(err) => {
            eprint!("Failed to establish a database connection and delete user: {}", err)
        }  
    }
}

async fn show_users() {
   println!("Showing users"); 

   let database_uri = dotenv!("DATABASE_URL");
   let db: Result<DbConn, DbErr> = establish_connection(database_uri).await;

    match db {
         Ok(db) => {
              let users: Vec<entity::user::Model> = entity::user::Entity::find()
                .all(&db)
                .await
                .unwrap();
    
              println!("Users: {:?}", users);
         },
         Err(err) => {
              eprint!("Failed to establish a database connection and show users: {}", err)
         }
    }
}
```
Most of the code we wrote in `user_ops.rs` has the same pattern. we establish database connection, then if we try to find/read first to the database then wanna do something to it we're gonna use an `Entity` to do database stuff then using `ActiveModel` if we want to have `ActiveValue` from it.

### Write `todo_ops` handler
In our `todo_ops.rs`, it will handle the `TodoCommand` and it will have a function that acts as an entry point that will responsible for a match enum based on the `TodoCommand` input whether it's CREATE or UPDATE or DELETE or READ, READBYUSERID. 

Based on my explanation, You know I just got a bit tired right?HAHAHAA. But I assure you that it's not so much different with `user_ops.rs`

Maybe there's a few I want to add that you gotta pay attention with `Set()` whenever we want to `update` or `create` an `ActiveModel`.

### Write `helper` module
This is actually not critical, but in my case I want to have a function that I'm gonna reuse it many times so I create a `helper` module to parse a date. 

File: src/helper/parse_date.rs
```rust
use chrono::NaiveDateTime;

// parse the date string into a NaiveDate
pub fn parse_date(date: &str) -> NaiveDateTime {
    let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, "%Y-%m-%d %H:%M:%S").unwrap();
    date
}
```

Also don't forget to create `mod.rs` inside `helper` module and make it public.
File: src/helper/mod.rs
```rust
pub mod parse_date;
```

## Step 6: Bringing it all together
Now we finally will bring it all into one place which is `src/main.rs`. Inside here gonna be a main entry from all the input command line we just created. We're gonna parse the arguments and will be using pattern matching based on the `entity type`. 

File: src/main.rs
```rust
mod args;
mod db;
mod ops;
mod helper;

use args::EntityType;
use args::PagaweanArgs;
use clap::Parser;
use ops::{todo_ops, user_ops};

#[tokio::main]
async fn main() {
    let args = PagaweanArgs::parse();

    match args.entity_type {
        EntityType::User(user) => user_ops::handle_user_command(user).await,
        EntityType::Todo(todo) => todo_ops::handle_todo_command(todo).await
    }
}
```
The magic is that clap will parse then handle it based on the enum SubCommand inside the pattern `match`-ing. The other magic is we're using `[tokio::main]` to have a `await` feature or asynchronous function.

Done. That is it. I know it's not a perfect code but I hope you'll get something from it. This post is not for you only to learn but also for me since I'm forgetful :) Definitely I'm gonna look at this post and try to relearn. 

LET'S BECOME A CRAB MASTER!!!🦀

![Prime](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3kiv3kaxiuxiqhizz8ff.png)


Code: https://github.com/masrayfa/rust-todo-clap