With Octocrab crate
Introduction
The github_api
repository demonstrates how to interact with the GitHub API using Rust. This guide will walk you through setting up a new Rust project and understanding the code in env.rs
, github.rs
, and main.rs
. By the end, you'll have a clear understanding of how to fetch commits from a GitHub repository using Rust. We’ll examine each file’s specific implementation details and how they work together.
including:
Async/await programming
Builder pattern implementation
Structured error handling
Environment-based configuration
Secure token management
Comprehensive logging
Note: To Install Rust if you do not have already installed, you may visit Rust Language web site and follow the instructions. Feel free to ask any question if you have.
Development
Setting Up the Project
To start, create a new Rust project using Cargo:
cargo new github_api
cd github_api
Cargo.toml
Next, update your Cargo.toml
file to include the necessary dependencies. These dependencies include octocrab
for interacting with the GitHub API, tokio
for asynchronous runtime, tracing
for logging, dotenv
for environment variable management, and anyhow
for error handling.
[package]
name = "github_api"
version = "0.1.0"
edition = "2021"
[dependencies]
dotenv = "0.15.0"
anyhow = "1.0.94"
tokio = { version = "1.42.0", features = ["full"] }
octocrab = "0.42.1"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
colored = "2.2.0"
env.rs
The env.rs
file handles the configuration of the GitHub API client by loading environment variables.
use anyhow::*;
use colored::Colorize;
use std::env::var;
/// A Config represents the configuration of the GitHub API client.
///
/// The Config should be generated from the user's environment, and should
/// contain the following fields:
///
/// * `github_token`: A valid GitHub API token.
///
pub struct Config {
github_token: String,
pub repo_owner: String,
pub repo_name: String,
pub log_level: String,
}
impl Config {
/// Loads configuration from environment variables
///
/// # Returns
/// A Result containing the Config if successful, or an error if any required
/// environment variables are missing
pub fn from_env() -> anyhow::Result<Self> {
dotenv::dotenv().ok();
let config = Config {
github_token: var("GITHUB_TOKEN")?,
repo_owner: var("REPO_OWNER").context("REPO_OWNER must be set")?,
repo_name: var("REPO_NAME").context("REPO_NAME must be set")?,
log_level: var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
};
config.validate().context(format!("{}","\nConfig is invalid".red().bold()))?;
Ok(config)
}
fn validate(&self) -> anyhow::Result<()> {
match self.github_token.is_empty() {
true => Err(anyhow!("GITHUB_TOKEN must be set")),
false => Ok(()),
}?;
match self.repo_owner.is_empty() {
true => Err(anyhow!("REPO_OWNER must be set")),
false => Ok(()),
}?;
match self.repo_name.is_empty() {
true => Err(anyhow!("REPO_NAME must be set")),
false => Ok(()),
}?;
match self.log_level.is_empty() {
true => Err(anyhow!("LOG_LEVEL must be set")),
false => Ok(()),
}?;
Ok(())
}
pub fn github_token(&self) -> String {
self.github_token.clone()
}
}
In this file, the Config
struct is defined to hold configuration details. The from_env
function loads these details from environment variables, and the validate
function ensures the necessary variables are set. The use of .context
with anyhow
provides additional context for error messages, making it easier to debug issues related to missing environment variables.
Key Implementation Details:
Uses
dotenv
for loading environment variablesImplements validation logic for each configuration field
Private
github_token
with public getter method for securityPublic fields for repository owner, name, and logging level
Add your variables into
.env
file and inlcude into.gitignore
file too
# GitHub Configuration
GITHUB_TOKEN=your_github_token
REPO_OWNER=dmbtechdev
REPO_NAME=github_api
# Logging Configuration
LOG_LEVEL=info
github.rs
The github.rs
file initializes the GitHub API client using the Octocrab library.
use octocrab::Octocrab;
use tracing::info;
use anyhow::Result;
#[derive(Debug)]
pub struct GitHubClientBuilder {
pub octocrab: Octocrab,
}
impl GitHubClientBuilder {
pub async fn new(token: String) -> Result<Self> {
let octocrab = Octocrab::builder()
.personal_token(token)
.build()?;
info!("Github Api Client initialized");
Ok(Self {
octocrab,
})
}
pub fn client(self) -> Octocrab {
self.octocrab
}
}
The GitHubClientBuilder
struct is used to set up the GitHub API client. The new
function creates an instance of Octocrab using the provided token, and the client
function returns the initialized client.
Technical Features:
Implements builder pattern for Octocrab client initialization
Asynchronous client creation with token authentication
Logging integration using
tracing
crateError handling with
anyhow::Result
lib.rs
Serves for our project.
pub mod github;
pub mod env;
main.rs
The main.rs
file is the entry point of the application, where the configuration is loaded, the client is initialized, and commits are fetched from the GitHub repository.
use std::{
error::Error,
time::Duration,
thread::sleep,
io::Write};
use anyhow::Result;
use colored::Colorize;
use github_api::{
github::*,
env::*};
use tracing::{info, error};
use tracing_subscriber::{
layer::SubscriberExt,
util::SubscriberInitExt};
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::from_env()?;
// Initialize the tracing subscriber
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
config.log_level.to_string()
))
.with(tracing_subscriber::fmt::layer().with_target(true))
.init();
// Clear the terminal
std::process::Command::new("clear").status().unwrap();println!("\n");
info!("Config: Repo Owner {}", config.repo_owner);
info!("Config: Repo Name {}", config.repo_name);
info!("Config: Log Level {}", config.log_level);
let github_client =
GitHubClientBuilder::new(config.github_token()).await?.client();
info!("GitHub Client is ready!");
for _ in 0..10 {
print!(".");
std::io::stdout().flush().unwrap();
sleep(Duration::from_millis(100));
}
println!("\n");
let commits =
github_client.repos( config.repo_owner, config.repo_name).list_commits().send().await;
std::process::Command::new("clear").status().unwrap();println!("\n");
match commits {
Err(e) => {
// Print the error message
error!("Error: {}", e);
// Print the source of the error
if let Some(source) = e.source() {
error!("Caused by: {}", source);
}
},
Ok(commits) => {
println!("Commits received");
for commit in commits {
println!("{}", commit.sha.green());
}
},
}
Ok(())
}
In main.rs
, the Config
is loaded and used to initialize logging. The GitHubClientBuilder
initializes the GitHub client, and the application fetches and prints the latest commits from the specified repository.
Key Technical Components:
Async Runtime:
Uses
tokio
with full featuresImplements async/await pattern
Error Handling:
Error chain handling
Error reporting with source tracking
Color-coded error messages
Logging System:
Structured logging with
tracing
Configurable log levels
Target-based logging
Output Formatting:
Terminal clearing for clean output
Progress indication with dots
Color-coded commit SHA display
Additional Functionalities
In addition to fetching commits, you can extend the functionality of your GitHub client to create issues, list pull requests, and fetch repository details. Here are some examples and you may try by yourselves
// Create an issue
let issue = github_client
.issues(config.repo_owner.clone(), config.repo_name.clone())
.create("New Issue Title")
.body("This is the body of the new issue.")
.send()
.await?;
info!("Created issue: {}", issue.html_url);
// List pull requests
let pulls = github_client
.pulls(config.repo_owner.clone(), config.repo_name.clone())
.list()
.send()
.await?;
for pull in pulls {
println!("Pull Request: {} - {:?}", pull.number, pull.title);
}
// Fetch repository details
let repo = github_client
.repos(config.repo_owner.clone(), config.repo_name.clone())
.get()
.await?;
info!("Repository details fetched: {:?}", repo.full_name);
Conclusion
By following the steps outlined above, you can set up a Rust project to interact with the GitHub API. The env.rs
file handles configuration, github.rs
initializes the client, and main.rs
fetches and displays commit information. This setup demonstrates a practical approach to integrating Rust with GitHub's API using environment variables and the Octocrab library.
Thank you and have a nice day.
You may find another implementation of Github API on X-Bot repo too. This one implements Twitter API too.
The next article that I wrote, “Reference(&) vs. Arc, Mutex, Rc, and RefCell with Long-Lived Data Structures in Rust”.
Please feel free to comment about the articles here or any particular subject you are looking for in Rust…
Top comments (0)