octocrate 0.1.6

A library of github API
Documentation
use crate::{
  GithubAPI, GithubInstallation, GithubInstallationAccessToken, GithubInstallationExpirableToken,
};
use octocrate_infra::{
  ExpirableToken, GithubAPIClient, GithubAPIConfig, GithubAPIRequest, GithubError, GithubResult,
};
use std::{
  collections::HashMap,
  sync::{Arc, Mutex, MutexGuard},
};

pub struct GithubAppInner {
  tokens: HashMap<u64, GithubInstallationAccessToken>,
}

#[derive(Clone)]
pub struct GithubApp {
  base_url: String,
  api_client: GithubAPIClient,
  inner: Arc<Mutex<GithubAppInner>>,
}

pub struct GithubAppBuilder {
  app_id: Option<String>,
  private_key: Option<String>,
  base_url: Option<String>,
}

impl GithubApp {
  pub fn builder() -> GithubAppBuilder {
    GithubAppBuilder::new()
  }

  pub fn new(
    app_id: impl Into<String>,
    private_key: impl Into<String>,
    base_url: impl Into<String>,
  ) -> Self {
    let token = GithubInstallationExpirableToken::new(app_id.into(), private_key.into());

    let tokens = HashMap::new();

    let base_url = base_url.into();
    let api_config = GithubAPIConfig::new(base_url.clone(), token);

    let inner = GithubAppInner { tokens };

    Self {
      base_url: base_url,
      api_client: GithubAPIClient::new(api_config),
      inner: Arc::new(Mutex::new(inner)),
    }
  }

  pub fn get_installations(&self) -> GithubAPIRequest<Vec<GithubInstallation>> {
    self
      .api_client
      .get::<Vec<GithubInstallation>>("/app/installations")
  }

  pub async fn get_repository_installation(
    &self,
    owner: impl Into<String>,
    repo: impl Into<String>,
  ) -> GithubResult<GithubInstallation> {
    let owner = owner.into();
    let repo = repo.into();

    let request_url = format!("/repos/{}/{}/installation", owner, repo);

    self
      .api_client
      .get::<GithubInstallation>(request_url)
      .send()
      .await
  }

  pub async fn get_installation(
    &self,
    installation_id: impl Into<u64>,
  ) -> GithubResult<GithubInstallation> {
    let request_url = format!("/app/installations/{}", installation_id.into());
    self
      .api_client
      .get::<GithubInstallation>(request_url)
      .send()
      .await
  }

  pub async fn get_installation_access_token(
    &self,
    installation_id: impl Into<u64>,
  ) -> GithubResult<GithubInstallationAccessToken> {
    let installation_id: u64 = installation_id.into();

    let inner = self.get_locked_inner()?.tokens.clone();

    if let Some(token) = inner.get(&installation_id) {
      if !token.is_expired() {
        return Ok(token.clone());
      }
    }

    let request_url = format!("/app/installations/{}/access_tokens", installation_id);

    let token = self
      .api_client
      .post::<GithubInstallationAccessToken>(request_url)
      .send()
      .await?;

    let mut inner = self.get_locked_inner()?;
    inner.tokens.insert(installation_id, token.clone());

    Ok(token)
  }

  pub async fn get_api(&self, installation_id: u64) -> GithubResult<GithubAPI> {
    let tokens = self.get_locked_inner()?.tokens.clone();
    if let Some(token) = tokens.get(&installation_id) {
      if !token.is_expired() {
        let api_config = GithubAPIConfig::new(self.base_url.clone(), token.clone());
        return Ok(GithubAPI::new(api_config));
      }
    }
    drop(tokens);

    let token = self.get_installation_access_token(installation_id).await?;
    let api_config = GithubAPIConfig::new(self.base_url.clone(), token.clone());
    return Ok(GithubAPI::new(api_config));
  }

  fn get_locked_inner(&self) -> Result<MutexGuard<GithubAppInner>, GithubError> {
    match self.inner.lock() {
      Ok(inner) => Ok(inner),
      Err(_) => Err(GithubError::new("Failed to get inner")),
    }
  }
}

impl GithubAppBuilder {
  pub fn new() -> Self {
    Self {
      app_id: None,
      private_key: None,
      base_url: None,
    }
  }

  pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
    self.app_id = Some(app_id.into());
    self
  }

  pub fn private_key(mut self, private_key: impl Into<String>) -> Self {
    self.private_key = Some(private_key.into());
    self
  }

  pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
    self.base_url = Some(base_url.into());
    self
  }

  pub fn build(self) -> GithubResult<GithubApp> {
    let app_id = self
      .app_id
      .ok_or_else(|| GithubError::new("app_id is required to create a GithubApp".to_string()))?;

    let private_key = self.private_key.ok_or_else(|| {
      GithubError::new("private_key is required to create a GithubApp".to_string())
    })?;

    let base_url = self
      .base_url
      .unwrap_or_else(|| "https://api.github.com".to_string());

    let app = GithubApp::new(app_id, private_key, base_url);

    Ok(app)
  }
}

#[cfg(test)]
mod tests {
  use crate::test_utils;
  use octocrate_infra::GithubResult;

  #[cfg(feature = "webhook-events")]
  #[tokio::test]
  #[ignore]
  async fn start() -> GithubResult<()> {
    let mut app = test_utils::create_github_app()?;

    app.start().await?;

    Ok(())
  }

  #[tokio::test]
  async fn get_installations() -> GithubResult<()> {
    let app = test_utils::create_github_app()?;

    let _installations = app.get_installations().send().await?;

    Ok(())
  }

  #[tokio::test]
  async fn get_installation() -> GithubResult<()> {
    let app = test_utils::create_github_app()?;
    let envs = test_utils::load_test_envs()?;

    let installation = app.get_installation(envs.installation_id).await?;

    assert_eq!(installation.id, envs.installation_id);
    Ok(())
  }

  #[tokio::test]
  async fn get_installation_access_token() -> GithubResult<()> {
    let app = test_utils::create_github_app()?;
    let envs = test_utils::load_test_envs()?;
    let installation_id = envs.installation_id;

    let access_token = app
      .get_installation_access_token(installation_id.clone())
      .await?;

    let access_token1 = app.get_installation_access_token(installation_id).await?;

    assert_eq!(access_token.expires_at, access_token1.expires_at);

    Ok(())
  }

  #[tokio::test]
  async fn get_repository_installation() -> GithubResult<()> {
    let app = test_utils::create_github_app()?;
    let envs = test_utils::load_test_envs()?;
    let github_app_id = envs.github_app_id;

    let installation = app
      .get_repository_installation(envs.repo_owner.clone(), envs.repo_name.clone())
      .await?;

    assert_eq!(installation.app_id.to_string(), github_app_id);

    Ok(())
  }

  #[tokio::test]
  async fn get_api() -> GithubResult<()> {
    let app = test_utils::create_github_app()?;
    let envs = test_utils::load_test_envs()?;
    let installation_id = envs.installation_id;
    let repo_installation = app
      .get_repository_installation("panghu-huang", "octocrate")
      .await?;

    let api1 = app.get_api(installation_id);
    let api2 = app.get_api(repo_installation.id);
    let _res = tokio::join!(api1, api2);

    Ok(())
  }
}