Building The TODO TCP Client

This chapter focuses on creating the TCP client. Switch to the TODO-Client directory in the workspace.

Configuration

Add the necessary dependencies to create the client.

$ cargo add async-std --features attributes

$ cargo add anyhow

$ cargo add bincode

$ cargo add serde --features derive

$ cargo add serde_json

bincode crate will be used to prepare the bytes of Command to send over the wire. serde_json crate will serialize the TODO list data structure that contains queued and completed TODOs into a JSON string for remote storage in the PostgreSQL database.

The TCP client will also store local cache, simulating a real world setup especially for a desktop or mobile client. SQLite will be the preferred database for this tutorial due to it's popularity. A command line frontend and a TCP stream will be used to keep the tutorial simple and easy to port to other domains like mobile device connection, desktop clients or HTTP clients if you wish to explore other domains.

Add sea-orm crate with the SQLite features enabled for the local persistent cache. The runtime features runtime-async-std-rustls are used since the async library for this client is async-std crate.

$ cargo add sea-orm  --features "runtime-async-std-rustls sqlx-sqlite macros" --no-default-features

Modify the main function in src/main.rs to use async-std

- fn main() {
-     println!("Hello, world!");
- }

+ #[async_std::main]
+ async fn main() -> anyhow::Result<()>{
+     Ok(())
+ }

Next, create a .env file in the current directory. This will contain the database configuration.

File: TODO-Client/.env

DATABASE_URL=sqlite://my_todos.db

Here, the sqlite URL does not take a username, password and IP since SQLite does not have have a server, just the database name my_todos.db.

Create an empty SQLite database using the command:

$ sqlite3 my_todos.db "VACUUM;"

The "VACUUM;" part of the command will ensure the created database is not just held in memory but also persisted to the file system even though it is empty.

Local SQLite Database Operations

Create a file src/db_ops.rs which will contain functions to perform database operations.

use async_std::{
    io::{ReadExt, WriteExt},
    net::TcpStream,
};
use sea_orm::{
    sea_query::{Alias, ColumnDef, Table},
    ActiveModelTrait, ConnectionTrait, Database, DatabaseConnection, EntityTrait, Set,
};

pub async fn database_config() -> Result<DatabaseConnection, sea_orm::DbErr> {
    // Read the database environment from the `.env` file
    let env_database_url = include_str!("../.env").trim();
    // Split the env url
    let split_url: Vec<&str> = env_database_url.split("=").collect();
    // Get item with the format `database_backend://username:password@localhost/database`
    let database_url = split_url[1];

    Database::connect(database_url).await
}


pub async fn create_todo_table(db: &DatabaseConnection) -> anyhow::Result<()> {
    let database_backend = db.get_database_backend();
    // Create the `todos` table
    let todos_table = Table::create()
        .table(Alias::new("todo_list"))
        .if_not_exists()
        .col(
            ColumnDef::new(Alias::new("todo_id"))
                .integer()
                .primary_key()
                .not_null()
                .auto_increment(),
        )
        .col(
            ColumnDef::new(Alias::new("todo_name"))
                .string()
                .unique_key()
                .not_null(),
        )
        .col(ColumnDef::new(Alias::new("quantity")).string().not_null())
        .col(ColumnDef::new(Alias::new("status")).boolean().not_null())
        .to_owned();
    let create_table_op = db.execute(database_backend.build(&todos_table)).await;

    // Executing the SQL query to create the `todos` table in SQLite
    let create_table_op = db.execute(database_backend.build(&todos_table)).await;
    // Print the result in a user friendly way
    println!(
        "`CREATE TABLE todo_list` {:?}",
        match create_table_op {
            Ok(_) => "Operation Successful".to_owned(),
            Err(e) => format!("Unsuccessful - Error {:?}", e),
        }
    );

    Ok(())
}

database_config() reads the .env file and parses the database URL, creates a database connection with the URL using Database::connect() and then returns a DatabaseConnection.

create_todo_table() when invoked will create a new todo_list table in the local SQLite database specified by the URL.

Import the db_ops module into src/main.rs and call both functions.

+ mod db_ops;
+ pub use db_ops::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
+	let db = database_config().await?;
+	create_todo_table(&db).await?;
    
    Ok(())
}

Next is to auto-generate the Model, ActiveModel , Entity, etc... using the sea-orm-cli and pass in --with-serde both feature flag to auto-generate serde::Serialize and serde::Deserialize on the Entity.

$ sea-orm-cli generate entity -o src/todo_list_table -t todo_list --with-serde both

This will create a new directory todo_list_table in the src/ directory.

Open the src/todo_list_table/prelude.rs file and import the Entity, Model and ActiveModel using friendly names.

File:src/todo_list_table/prelude.rs

//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0

- pub use super::todo_list::Entity as TodoList;

+ pub use super::todo_list::{
+     ActiveModel as MyTodosActiveModel, Column as MyTodosColumn, Entity as MyTodos,
+     Model as MyTodosModel, PrimaryKey as MyTodosPrimaryKey, Relation as MyTodosRelation,
+ };

Import the modules to the src/main.rs file

  mod db_ops;
+ mod todo_list_table;

  pub use db_ops::*;
+ pub use todo_list_table::prelude::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

    Ok(())
}

Common Data Structures

To perform more database operations, create a common.rs file in the src directory. This file will contain common data structures for use throughout database operations and TCP connections.

File: src/common.rs

use crate::MyTodosModel;
use serde::{Deserialize, Serialize}; // The commands to use to perform CRUD operations on PostgreSQL

// The commands to use to perform CRUD operations on PostgreSQL
#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Store { username: String, todo_list: String },
    UpdateTodoList { username: String, todo_list: String },
    Get(String),
    CreateUser(String),
    ListFruits,
}

//  The structure for a TodoList
#[derive(Debug, Serialize, Default, Deserialize)]
pub struct TodoList {
    pub queued: Vec<MyTodosModel>,
    pub completed: Vec<MyTodosModel>,
}

The enum Command mirrors the Command created in the previous chapter in the TODO-Server/src/tcp_api.rs file.

The TodoList struct contains the Models MyTodoModel sorted either as queued which are TODOs not done or completed which are TODOs that are done.

Import this file to the src/main.rs file

+ mod common;
  mod db_ops;
  mod todo_list_table;

+ pub use common::*;
  pub use db_ops::*;
  pub use todo_list_table::prelude::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

    Ok(())
}