Octocat

Octocat aims to be the GitHub API library for Rust. It was created due to a lack of well-maintained alternatives.

Design Overview

  • There are two modes of operation for Octocat.

  • The first (and most common) one is as a webhook event listener:

    • Essentially, the event listener passes payloads to the user, who then responds with a Command that can either contain nothing or a number of futures.
    • The Command is executed as soon as the event loop is free, and the user recieves the result in the form of a Message (an enum which they define).

Visualization

                                             ┌────────────────┐  ┌────────┐
┌──────────────┐  ┌─────┐   ┌─────────────┐◄─┤Result (Message)├──┤        │
│Event Listener├──┤Event├──►│Event Handler│  └────────────────┘  │Executor│
└──────────────┘  └─────┘   └───┬─────────┴─┐                  ┌►│        │
                                │      ▲    └──────────────────┘ └────────┘
                                │      │
                                │  ┌───┴───┐
                                │  │Command│
                                │  └───┬───┘
                                ▼      │
                               ┌───────┴┐
                               │  User  │
                               └────────┘
  • The second is as a glorified HTTP client wrapper.

Info

  • If you're using Octocat for Cloudflare Workers, click here.
  • If aren't, click here.

Note

Octocat currently relies on some nightly features (namely associated type defaults) to build. Set the toolchain override in the project directory to nightly. You can do this via:

  • The rustup command, or
rustup override set nightly
  • The rust-toolchain.toml file (recommended):
[toolchain]
channel = "nightly"

Getting Started (native)

  • Create the project
cargo new octocat-app --bin
cd octocat-app
  • Add octocat, tokio, and async-trait to your dependencies in Cargo.toml
[dependencies]
octocat-rs = { git = "https://github.com/octocat-rs/octocat-rs" }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1.52"

Getting Started (Workers)

Octocat has support for WebAssembly with Cloudflare workers. See the cloudflare example in the project repository for more information.

Fundamental Concepts

This chapter goes over the concepts you'll need to understand in order to use Octocat.

Understanding EventHandler

EventHandler is a trait you must implement if you intend to use the event listener portion of Octocat in any meaningful way.

Let's break down what you need to know.

Associated types

Message

  • The enum which you recieve when a future from a Command is completed.
trait EventHandler {
    type Message: std::fmt::Debug + Send,

    ...

    async fn message(&self, message: Self::Message) {
        {}
    }

    ...
}

GitHubClient

Note

In 99% of cases, this type should be set equal to Client<Self>.

  • Client<Self> is the default value. You shouldn't have to override this unless you're writing your own custom GitHubClient implementation (not recommended)
  • This is used to represent the type of the github_client parameter present in each event.
#[async_trait]
trait EventHandler {
    type GitHubClient: GitHubClient + Send + Sync;

    ...

    async fn example_event(
        &self,
        github_client: Arc<Self::GitHubClient>,
        example_event: ExampleEventType,
    ) -> Command<Self::Message> {
        Command::none()
    }
    
    ...
}

Listener Configuration

Supported on all platforms.

listener_secret

Info

  • Requires the secrets feature to be enabled.
  • Your webhook secret.
trait EventHandler {
    fn listener_secret(&self) -> &[u8] {
        "secret".as_bytes()
    }

    ...
}

Unsupported on WebAssembly

listener_port

  • The port where the event listener listens (8080 by default).
trait EventHandler {
    fn listener_port(&self) -> u16 {
        8080
    }

    ...
}

route

  • The route at which payloads are to be accepted (/payload by default).
trait EventHandler {
    fn route(&self) -> &'static str {
        // There is no need to prepend a / to the route
        "payload"
    }

    ...
}

Events

  • There are functions for each possible webhook event, and they all follow the same format. Here's the ping event as an example:
#[async_trait]
trait EventHandler {
    async fn ping_event(
        &self, 
        github_client: Arc<Self::GitHubClient>,
        ping_event: PingEvent,
    ) -> Command<Self::Message> {
        Command::none()
    }

    ...
}

Usage Example

Example

Here's an example of the EventHandler trait in action.

use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;

use octocat_rs::{handler::EventHandler, rest::model::repositories::events::PushEvent, Client, ClientBuilder, Command};

#[derive(Debug)]
struct Handler {}

#[derive(Debug)]
enum Message {
    Stuff(&'static str),
}

#[async_trait]
impl EventHandler for Handler {
    type Message = Message;
    type GitHubClient = Client<Self>;

    fn listener_port(&self) -> u16 {
        2022
    }

    async fn message(&self, message: Self::Message) {
        match message {
            Message::Stuff(s) => {
                println!("==> Message received: {s}");
            }
        }
    }

    async fn commit_event(
        &self,
        github_client: Arc<Self::GitHubClient>,
        commit: PushEvent,
    ) -> Command<Self::Message> {
        println!("Commit pushed!");

        Command::perform(async { "Computation finished" }, Message::Stuff)
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    ClientBuilder::new().event_handler(Handler {}).build()?.start().await;

    Ok(())
}

Understanding Builders

All builders in Octocat follow a similar pattern. Each one implements the Builder trait, and contains setters for each of its fields, nested or not.

Warning

Builders and methods are currently limited in their variety. This will change with time, however it may take a while for work to resume.

The Builder trait

#[async_trait]
pub trait Builder {
    type Response: DeserializeOwned;

    async fn execute<T>(self, client: &T) -> Result<Self::Response, GithubRestError>
    where
        T: Requester;
}

Examples

Example

Commenting on a commit.

use octocat_rs::{
  rest::{
      builders::{CommitCommentBuilder, Builder},
      model::reactions::Reaction,
      client::DefaultRequester,
  },
};

let res = CommitCommentBuilder::new()
    .owner("octocat-rs")
    .repo("octocat-rs")
    .sha("fcc8348f8286d05976090a9086d64eefb90e3f8b")
    .body("Some text here")
    .execute(&DefaultRequester::new("TOKEN"))
    .await
    .unwrap();

// Prints the URL at which you can find the comment you've just made.
dbg!(res.html_url);

Example

Reacting to a commit comment.

use octocat_rs::{
  rest::{
      builders::{CommentReactionBuilder, Builder},
      model::reactions::Reaction,
      client::DefaultRequester,
  },
};

let _ = CommentReactionBuilder::new()
    .owner("octocat-rs")
    .repo("octocat-rs")
    .comment_id(67534661)
    .reaction(Reaction::Rocket)
    .execute(&DefaultRequester::new("TOKEN"))
    .await
    .unwrap();

// Comment you just reacted to: https://github.com/octocat-rs/octocat-rs/commit/40919cbf40530cf15a002d701a1a1bd6a6006105#commitcomment-67534661

Comparison with Octocrab

Octocrab is the closest competitor in the Rust ecosystem to Octocat. There are, however, some major differences between the two. Let's go over them:

  • Octocrab is an API Client, whereas Octocat can serve as multiple things, including an API client and event listener.
  • Octocrab's API is currently more intuitive and polished than Octocat's, however it's not as comprehensive.

Info

We are working on the polish, however it will take some time before we can say that we're on-par with the competition here.

  • Octocat has support for Cloudflare Workers, whereas Octocrab does not.

  • Octocat's API is also designed to be much more flexible, something that can be seen in how our builders are structured.

Todo

Detailed comparisons and examples are coming soon; it'll take time for me to become familiar enough with their API to write them.

Direct API Comparisons

Warning

This section is still a WIP.

Example: Getting 50 issues from a repository

Octocrab

octocrab::instance()
    .issues("microsoft", "vscode")
    .list()
    .per_page(50)
    .send()
    .await?;

Octocat

use octocat_rs::{
  rest::builders::{
      Builder, GetIssuesBuilder
  },
  HttpClient, 
};

let http_client = HttpClient::new_none();

GetIssuesBuilder::new()
    .owner("microsoft")
    .repo("vscode")
    .per_page(50.to_string())
    .execute(&http_client)
    .await?;

As you can see from this example, there is a key difference between how builders function in Octocrab and in Octocat.

  • In Octocrab, the HTTP client is the source of the builder.
  • In Octocat, the builder is instantianted separately, with the HTTP client only attached when it's time to execute it. We made this choice because we believe it's more flexible than the alternative; it allows for users to conditionally attach authorization to a request.

Speaking of authorization, let's compare how you would go about adding authorization to an HTTP client in the two.

Example: Attaching Authorization to an HTTP Client

Octocrab

use octocrab::Octocrab;

Octocrab::builder().personal_token("TOKEN").build()?;

Octocat

use octocat_rs::{HttpClient, Authorization}

let username: String;
let token: String;

let auth = Authorization::PersonalToken { username, token };

HttpClient::new(Some(auth), None);

You might notice that Octocat requires the username whereas Octocrab does not. This is due to a difference in internal design choices, however support for the former option would be easy to implement in Octocat if it is desired.