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:
Visualization
┌────────────────┐ ┌────────┐
┌──────────────┐ ┌─────┐ ┌─────────────┐◄─┤Result (Message)├──┤ │
│Event Listener├──┤Event├──►│Event Handler│ └────────────────┘ │Executor│
└──────────────┘ └─────┘ └───┬─────────┴─┐ ┌►│ │
│ ▲ └──────────────────┘ └────────┘
│ │
│ ┌───┴───┐
│ │Command│
│ └───┬───┘
▼ │
┌───────┴┐
│ User │
└────────┘
- The second is as a glorified HTTP client wrapper.
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 customGitHubClient
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.
- The
GitHubClient
andRequester
traits also demonstrate this.
- The
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
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.