Skip to content

Commit

Permalink
Add skeleton documentation and add clippy warning to workflow (#4)
Browse files Browse the repository at this point in the history
* init docs

* warn on missing docs

* add rustdoc lints to github actions

* remove rustdoc lints, clippy sufficient

* remove docs from dependent docs

* remove pre-commit hook for docs

* fix clippy warnings
  • Loading branch information
reubenwong97 authored Aug 26, 2023
1 parent d101ab8 commit 164e170
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 12 deletions.
65 changes: 54 additions & 11 deletions src/bot.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Bot services.
//!
//! The shuttle_runtime for the BotService is defined here, which
//! binds itself to the `SocketAddr` provided by shuttle.
use crate::{
database::*,
message::{display_tally, list_runs, list_users},
Expand All @@ -6,11 +10,15 @@ use sqlx::PgPool;
use teloxide::{prelude::*, utils::command::BotCommands};
use tracing::error;

/// Encapsulate the BotService.
pub struct BotService {
/// Teloxide Bot.
pub bot: Bot,
/// Database connection.
pub postgres: PgPool,
}

/// Required implementation of the `shuttle_runtime::Service` trait for `BotService`.
#[shuttle_runtime::async_trait]
impl shuttle_runtime::Service for BotService {
async fn bind(self, _addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> {
Expand All @@ -23,7 +31,11 @@ impl shuttle_runtime::Service for BotService {
}
}

/// impl block for `BotService`.
impl BotService {
/// Clones `bot` and `db_connection` before passing these over to `Command`, also
/// defined within teloxide. It parses incoming commands, matches them and
/// hands them over to the `answer` methold.
async fn start(&self) -> Result<(), shuttle_runtime::CustomError> {
let bot = self.bot.clone();
let db_connection = self.postgres.clone();
Expand All @@ -37,31 +49,62 @@ impl BotService {
}
}

/// Enumeration of commands accepted by the bot.
#[derive(BotCommands, Clone)]
#[command(
rename_rule = "lowercase",
description = "The following commands are supported:"
)]
enum Command {
#[command(description = "display this text")]
/// Matched to `/help` -> displays commands and their documentation.
#[command(description = "Display this text. Usage: /help")]
Help,
#[command(description = "Show users registered on telerun within the chat.")]
#[command(description = "Show users registered on telerun within the chat. Usage: /show")]
/// Matched to `/show` -> displays users within chat.
Show,
/// Matched to `/add <distance> <user_name>` -> creates users in db if not present,
/// then adds run data to runs table.
#[command(
description = "Add run data to database. Format is /add %distance (km)% %username%",
description = "Add run data to database. Usage: /add <distance> <user_name>",
parse_with = "split"
)]
Add { distance: f32, user_name: String },
#[command(description = "Edit data for a run.", parse_with = "split")]
Edit { run_id: i32, distance: f32 },
#[command(description = "Remove a run from database.")]
Delete { run_id: i32 },
#[command(description = "Tallies current medals and distances.")]
Add {
/// Distance run in km
distance: f32,
/// Name of user to tie the run to. Must be unique.
user_name: String,
},
/// Matched to `/edit <run_id> <distance>` -> edits stored run data.
#[command(
description = "Edit data for a run. Usage: /edit <run_id> <distance>",
parse_with = "split"
)]
Edit {
/// Id of run as stored in runs table.
run_id: i32,
/// Corrected distance run in km.
distance: f32,
},
/// Matched to `/delete <run_id>` -> removes a certain run from database.
#[command(description = "Remove a run from database. Usage: /delete <run_id>")]
Delete {
/// Id of run to remove from table.
run_id: i32,
},
/// Matched to `/tally` -> sends score board as message through Telegram.
#[command(description = "Tallies current medals and distances. Usage: /tally")]
Tally,
#[command(description = "Lists recent runs. Number of runs to display must be specified.")]
List { limit: u32 },
/// Matched to `/list <limit>` -> displays runs registered by the group chat, subject to a limit.
#[command(
description = "Lists recent runs. Number of runs to display must be specified. Usage: /list <num_runs_to_show>"
)]
List {
/// Limit to query from db.
limit: u32,
},
}

/// Function used for handling various commands matched.
async fn answer(bot: Bot, msg: Message, cmd: Command, db_connection: PgPool) -> ResponseResult<()> {
match cmd {
Command::Help => {
Expand Down
39 changes: 38 additions & 1 deletion src/database.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
//! Database operations.
//!
//! [sqlx](https://docs.rs/sqlx/latest/sqlx/) is used to interact with the
//! Postgresql database. Macros are used to check queries against the
//! database at compile time.
use crate::models::{Run, Score, User};
use sqlx::PgPool;
use teloxide::types::ChatId;
use tracing::error;

/// Convenience type to wrap a generic `Ok` and `sqlx::Error`.
type DBResult<T> = Result<T, sqlx::Error>;

/// Creates a user in users table.
///
/// Users are tied to the `chat_id` that the message came from
/// and the `user_name` input. This combination must be unique.
pub async fn create_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"INSERT INTO users (chat_id, user_name)
Expand All @@ -19,6 +29,9 @@ pub async fn create_user(user_name: &str, chat_id: ChatId, connection: &PgPool)
Ok(())
}

/// Retrieves a user.
///
/// Fetches user information based on `(user_name, chat_id)`.
async fn get_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBResult<Option<User>> {
let user: Option<User> = sqlx::query_as!(
User,
Expand All @@ -34,6 +47,9 @@ async fn get_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBRe
Ok(user)
}

/// Fetchers users in a chat.
///
/// Retrieves users in a chat from `ChatId`.
pub async fn get_users_in_chat(
chat_id: ChatId,
connection: &PgPool,
Expand Down Expand Up @@ -61,7 +77,19 @@ pub async fn get_users_in_chat(
}
}

// TODO: refactor in the future
/// Wrapper for adding run data.
///
/// # Arguments
/// * `distance` - Distance run in km
/// * `user_name` - Name user wishes to tie the run to.
/// * `chat_id` - Unique ID identifying the chat, this comes from Telegram.
///
/// # Remarks
///
/// Due to design flaws, we first check whether that user has
/// added a run before. If not, we need to first create that user.
/// Afterwards, we run `get_user` again to retrieve its `user_id`.
/// Following which, we then actually add the run to the database.
pub async fn add_run_wrapper(
distance: f32,
user_name: &str,
Expand All @@ -85,6 +113,9 @@ pub async fn add_run_wrapper(
Ok(())
}

/// Adds run data.
///
/// Performs the actual database update for adding run data.
async fn add_run(distance: f32, user_id: i32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"INSERT INTO runs (distance, user_id)
Expand All @@ -99,6 +130,9 @@ async fn add_run(distance: f32, user_id: i32, connection: &PgPool) -> DBResult<(
Ok(())
}

/// Fetches runs fromt the chat.
///
/// `limit` must be specified or the `answer` cannot match the enum.
pub async fn get_runs(
chat_id: ChatId,
limit: i64,
Expand Down Expand Up @@ -137,6 +171,7 @@ pub async fn get_runs(
}
}

/// Updates a certain run by id.
pub async fn update_run(run_id: i32, distance: f32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"UPDATE runs
Expand All @@ -151,6 +186,7 @@ pub async fn update_run(run_id: i32, distance: f32, connection: &PgPool) -> DBRe
Ok(())
}

/// Deletes a run by id.
pub async fn delete_run(run_id: i32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"DELETE FROM runs
Expand All @@ -163,6 +199,7 @@ pub async fn delete_run(run_id: i32, connection: &PgPool) -> DBResult<()> {
Ok(())
}

/// Aggregates runs into a tally (`Vec<Score>`)
pub async fn get_tally(chat_id: ChatId, connection: &PgPool) -> DBResult<Option<Vec<Score>>> {
let users = get_users_in_chat(chat_id, connection).await?;

Expand Down
18 changes: 18 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]

//! Telegram bot for a running contest between friends.
//!
//! Implements a telegram bot hosted on shuttle.rs (subject to change).
//! Shuttle provisions infrastructure from our infrastructure as code
//! that is used in this codebase.

mod bot;
mod database;
mod message;
Expand All @@ -8,6 +17,15 @@ use shuttle_secrets::SecretStore;
use sqlx::PgPool;
use teloxide::prelude::*;

/// Entry point to the telegram bot service.
///
/// We pass in the resources that we wish to provision as arguments to `shuttle_main()`.
/// As a requirement of shuttle.rs for provisioning a database, we run sqlx migrations
/// as the first step as well.
///
/// Next, we load in our telegram bot's key and as it is a requirement for teloxide.
///
/// Finally, we start our service.
#[shuttle_runtime::main]
async fn shuttle_main(
#[shuttle_secrets::Secrets] secrets: SecretStore,
Expand Down
29 changes: 29 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
//! Templating and message preparation.
//!
//! This module reads in Rust-native objects and renders
//! them as `String`s using [askama](https://crates.io/crates/askama/0.7.2)
//! as the templating engine.

use crate::models::{Run, Score, User};
use askama::Template;
use std::fmt;
use std::ops;

/// NewType implementation so that Display can be implemented for it.
struct RunDisplay(Run);

/// Deref is implemented so that accessing `Run`'s contents is easier.
impl ops::Deref for RunDisplay {
type Target = Run;

Expand Down Expand Up @@ -42,12 +50,19 @@ impl fmt::Display for Score {
}
}

/// Struct Run display.
#[derive(Template)]
#[template(path = "list_runs.j2")]
struct ListRunTemplate<'a> {
/// Reference to `runs` for askama to access.
runs: &'a Vec<RunDisplay>,
}

/// Displays runs fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there are no runs
/// stored in the database.
pub fn list_runs(runs: Option<Vec<Run>>) -> String {
if let Some(runs) = runs {
let run_displays: Vec<RunDisplay> = runs.into_iter().map(RunDisplay).collect();
Expand All @@ -61,12 +76,19 @@ pub fn list_runs(runs: Option<Vec<Run>>) -> String {
}
}

/// Struct User display.
#[derive(Template)]
#[template(path = "list_users.j2")]
struct ListUserTemplate<'a> {
/// Reference to `users` for askama to access.
users: &'a Vec<User>,
}

/// Displays users fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there are no users
/// stored in the database.
pub fn list_users(users: Option<Vec<User>>) -> String {
if let Some(users) = users {
let user_template = ListUserTemplate { users: &users };
Expand All @@ -77,12 +99,19 @@ pub fn list_users(users: Option<Vec<User>>) -> String {
}
}

/// Struct Tally display.
#[derive(Template)]
#[template(path = "list_tally.j2")]
struct ListTallyTemplate<'a> {
/// Reference to `scores` for askama to access.
scores: &'a Vec<Score>,
}

/// Displays score aggregates fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there the tally
/// cannot be generated.
pub fn display_tally(scores: Option<Vec<Score>>) -> String {
if let Some(scores) = scores {
let tally_template = ListTallyTemplate { scores: &scores };
Expand Down
21 changes: 21 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
//! Struct models for database tables.
//!
//! Contains structs for an "ORM-like" approach to
//! database interactions.

use sqlx::types::chrono;

/// Represents a user row in the `users` table.
#[derive(sqlx::FromRow)]
pub struct User {
/// User id
pub id: i32,
/// Id of telegram chat
pub chat_id: String,
/// Self-specified username
pub user_name: String,
}

/// Represents a run row in the `runs` table.
#[derive(sqlx::FromRow)]
pub struct Run {
/// Run id
pub id: i32,
/// Distance ran for a particular run
pub distance: f32,
/// Datetime when the run was submitted to the database
pub run_datetime: Option<chrono::NaiveDateTime>,
/// User_id of the user who submitted the run
pub user_id: i32,
}

/// Represents a score that appears in the tally.
///
/// While this struct those not correspond direclty to a database
/// table, it is built directly from results retrieved.
pub struct Score {
/// Self-specified username
pub user_name: String,
/// Number of runs for the user, or in this case, medals
pub medals: u32,
/// Total distance run by the user
pub distance: f32,
}

0 comments on commit 164e170

Please sign in to comment.