eversal-esi 0.1.2

Eve Online's ESI API library for Rust and Eversal projects
Documentation
use std::{collections::HashMap, sync::OnceLock, time::Duration};

use log::{trace, warn};
use reqwest::Client;
use serde::{de::DeserializeOwned, Serialize};

pub mod auth;
pub mod error;
pub use error::Error;
pub mod model;
pub mod routes;

pub type EsiResult<T> = Result<T, error::Error>;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Paged<T> {
  pub data: T,
  pub page: i32,
  pub total_pages: i32,
}

const ESI_URL: &str = "https://esi.evetech.net";
const ESI_DATASOURCE: &str = "tranquility";
const TIMEOUT: u64 = 10;
const CLIENT: OnceLock<Client> = OnceLock::new();
const USER_AGENT: OnceLock<String> = OnceLock::new();

pub fn client() -> Client {
  CLIENT.get_or_init(|| Client::new()).clone()
}

pub fn user_agent() -> String {
  USER_AGENT
    .get_or_init(|| format!("{} ({})", "eversal", "contact@eversal.io"))
    .clone()
}

pub fn initialize(application_name: String, application_email: String) {
  match CLIENT.set(Client::new()) {
    Ok(_) => (),
    Err(_) => warn!("Client already initialized"),
  };
  match USER_AGENT.set(format!("{} ({})", application_name, application_email)) {
    Ok(_) => (),
    Err(_) => warn!("User agent already initialized"),
  };
}

pub async fn get_public<T: DeserializeOwned>(
  path: &str,
  params: Option<HashMap<&str, String>>,
) -> Result<T, reqwest::Error> {
  let url = build_url(path, params);
  trace!("Requesting: {}", url);
  let req = client()
    .get(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .timeout(std::time::Duration::from_secs(TIMEOUT))
    .send()
    .await?;

  req.error_for_status_ref()?;
  let result: T = req.json().await?;
  Ok(result)
}

pub async fn get_public_paged<T: DeserializeOwned>(
  path: &str,
  params: Option<HashMap<&str, String>>,
) -> Result<Paged<T>, reqwest::Error> {
  let page = match &params {
    Some(params) => match params.get("page") {
      Some(page) => match page.parse::<i32>() {
        Ok(page) => page,
        Err(_) => 1,
      },
      None => 1,
    },
    None => 1,
  };
  let url = build_url(path, params);
  trace!("Requesting: {}", url);
  let req = client()
    .get(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .timeout(std::time::Duration::from_secs(TIMEOUT))
    .send()
    .await?;

  req.error_for_status_ref()?;
  // Pages header
  let total_pages = match req.headers().get("X-Pages") {
    Some(pages) => match pages.to_str() {
      Ok(pages) => match pages.parse::<i32>() {
        Ok(pages) => pages,
        Err(_) => 1,
      },
      Err(_) => 1,
    },
    None => 1,
  };
  let result: T = req.json().await?;
  Ok(Paged {
    data: result,
    page,
    total_pages,
  })
}

pub async fn get_authenticated<T: DeserializeOwned>(
  access_token: &str,
  path: &str,
  params: Option<HashMap<&str, String>>,
) -> Result<T, reqwest::Error> {
  let url = build_url(path, params);
  trace!("Requesting: {}", url);
  let req = client()
    .get(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .header(
      reqwest::header::AUTHORIZATION,
      format!("Bearer {}", access_token),
    )
    .timeout(std::time::Duration::from_secs(TIMEOUT))
    .send()
    .await?;

  req.error_for_status_ref()?;
  let result: T = req.json().await?;
  Ok(result)
}

pub async fn get_authenticated_paged<T: DeserializeOwned>(
  access_token: &str,
  path: &str,
  params: Option<HashMap<&str, String>>,
) -> Result<Paged<T>, reqwest::Error> {
  let page = match &params {
    Some(params) => match params.get("page") {
      Some(page) => match page.parse::<i32>() {
        Ok(page) => page,
        Err(_) => 1,
      },
      None => 1,
    },
    None => 1,
  };
  let url = build_url(path, params);
  trace!("Requesting: {}", url);
  let req = client()
    .get(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .header(
      reqwest::header::AUTHORIZATION,
      format!("Bearer {}", access_token),
    )
    .timeout(std::time::Duration::from_secs(TIMEOUT))
    .send()
    .await?;

  req.error_for_status_ref()?;
  let total_pages = match req.headers().get("X-Pages") {
    Some(pages) => match pages.to_str() {
      Ok(pages) => match pages.parse::<i32>() {
        Ok(pages) => pages,
        Err(_) => 1,
      },
      Err(_) => 1,
    },
    None => 1,
  };
  let result: T = req.json().await?;
  Ok(Paged {
    data: result,
    page,
    total_pages,
  })
}

pub async fn post_public<T: DeserializeOwned, U: Serialize + ?Sized>(
  path: &str,
  params: Option<HashMap<&str, String>>,
  data: &U,
) -> Result<T, reqwest::Error> {
  let url = build_url(path, params);
  trace!(
    "Requesting: {} with data: {}",
    url,
    serde_json::to_string(data).unwrap()
  );
  let req = client()
    .post(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .timeout(std::time::Duration::from_secs(TIMEOUT))
    .json(data)
    .send()
    .await?;

  req.error_for_status_ref()?;
  let result: T = req.json().await?;
  Ok(result)
}

pub async fn post_authenticated<T: DeserializeOwned, U: Serialize + ?Sized>(
  access_token: &str,
  path: &str,
  params: Option<HashMap<&str, String>>,
  data: &U,
) -> Result<T, reqwest::Error> {
  let url = build_url(path, params);
  trace!(
    "Requesting: {} with data: {}",
    url,
    serde_json::to_string(data).unwrap()
  );
  let req = client()
    .post(url)
    .header(reqwest::header::USER_AGENT, user_agent())
    .header(
      reqwest::header::AUTHORIZATION,
      format!("Bearer {}", access_token),
    )
    .timeout(Duration::from_secs(TIMEOUT))
    .json(data)
    .send()
    .await?;

  req.error_for_status_ref()?;
  let result: T = req.json().await?;
  Ok(result)
}

fn build_url(path: &str, params: Option<HashMap<&str, String>>) -> String {
  let mut url = format!("{}/latest/{}?datasource={}", ESI_URL, path, ESI_DATASOURCE);
  if let Some(params) = params {
    for (key, value) in params {
      url.push_str(&format!("&{}={}", key, value));
    }
  }
  url
}