DEV Community

Cover image for Using the GitHub API in Rust
Baris @dmbtechdev
Baris @dmbtechdev

Posted on • Originally published at Medium

Using the GitHub API in Rust

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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()
    }

}
Enter fullscreen mode Exit fullscreen mode

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 variables

  • Implements validation logic for each configuration field

  • Private github_token with public getter method for security

  • Public 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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 crate

  • Error handling with anyhow::Result

lib.rs

Serves for our project.

pub mod github;
pub mod env;
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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 features

  • Implements 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);
Enter fullscreen mode Exit fullscreen mode

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…

and you can find me on X account and on Github.

Top comments (0)