Skip to content

Design philosophy

SnejUgal edited this page Dec 21, 2019 · 5 revisions

We, the two people behind tbot, want to create a crate which helps to develop Telegram bots with Rust quickly and easily. Here's our vision of what tbot should be:

  • Easy-to-use: tbot should abstract away the underlying parts of the API, so users of tbot can focus on the logic of their bot without knowing what's under the hood.
  • Type-safe: tbot's API must be designed in such a way that prevents invalid type states and method arguments.
  • Future-proof: tbot's design must easily adapt for the future.
  • Fast: tbot must do as little work as possible, and do it as quickly as possible.

Given this, tbot's design implements several patterns and design decisions.

Modular design

One part of tbot must have only one purpose. For example, methods::SendChatAction only does what the sendChatAction can do: sending one chat action. It won't send the chat action repeatedly, because the purpose of the methods module is to provide plain methods.

Builder API

In many cases, there are optional arguments to methods. In these cases, a Builder API must be provided. For example, the sendMessage methods has several optional arguments. It's much easier to set optional arguments with Builder API:

use tbot::{types::parameters::Text, prelude::*};

let bot = tbot::from_env!("BOT_TOKEN");
bot.send_message(CHAT_ID, Text::markdown("`tbot` is amazing!")).call().await;

than if you passed everything to the constructor:

use tbot::{
    methods::SendMessage,
    types::parameters::ParseMode::Markdown,
    prelude::*,
};

let bot = tbot::from_env!("BOT_TOKEN");
bot.send_message(
    TOKEN,
    CHAT_ID,
    "`tbot` is amazing!",
    Some(Markdown),
    None,
    None,
    None,
    None,
)
.call()
.await;

Borrowing, not owning

tbot must take ownership only when it is essential. For example, Bot takes ownership of the token because it needs to put the token in an Arc, and then passes it around when needed.

Types prevent invalid states

tbot's types must prevent invalid states. For example, consider the InlineKeyboardButton type. The docs list all (but one) fields as optional but they state:

You must use exactly one of the optional fields.

With a bad library, constructing such a type would look like this:

let button = InlineKeyboardButton::new("Rust good")
    .callback_data("rust_good");

What if one doesn't call callback_data or a similar method? What if one calls callback_data and login_url? Most likely, you won't even notice it before you make a request. How would a good library solve this? One way is this:

let button = inline::Button::new(
    "Rust good",
    inline::ButtonKind::Callback("rust_good"),
);

You can see that this way it's impossible to pass several types; if you pass none, you get a clear compile-time error.

Clone this wiki locally