pub mod private;
pub mod public;
use crate::{
auth::Auth, constants::API_BASE_URL, request::{AccessLevel, Request, RequestMethod}, utils
};
use anyhow::{anyhow, Result};
use ring::hmac;
use serde::{de::DeserializeOwned, Serialize, Deserialize};
use reqwest::header::{HeaderMap, HeaderValue};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct GmoApi {
base_url: String,
api_key: Option<String>,
api_secret: Option<String>,
}
impl GmoApi {
pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {
GmoApi {
base_url: API_BASE_URL.to_string(),
api_key,
api_secret,
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn api_key(&self) -> &Option<String> {
&self.api_key
}
pub fn api_secret(&self) -> &Option<String> {
&self.api_secret
}
async fn get<T: Serialize + Clone, P: DeserializeOwned>(&self, path: &str, parameters: Option<T>, access_level: AccessLevel) -> Result<P> {
let body = self.send_request(RequestMethod::GET, path, parameters, access_level).await?;
self.deserialize_response(&body)
}
async fn post<T: Serialize + Clone, P: DeserializeOwned>(&self, path: &str, parameters: Option<T>, access_level: AccessLevel) -> Result<P> {
let body = self.send_request(RequestMethod::POST, path, parameters, access_level).await?;
self.deserialize_response(&body)
}
async fn put<T: Serialize + Clone, P: DeserializeOwned>(&self, path: &str, parameters: Option<T>, access_level: AccessLevel) -> Result<P> {
let body = self.send_request(RequestMethod::PUT, path, parameters, access_level).await?;
println!("body: {:?}", body);
self.deserialize_response(&body)
}
async fn delete<T: Serialize + Clone, P: DeserializeOwned>(&self, path: &str, parameters: Option<T>, access_level: AccessLevel) -> Result<P> {
let body = self.send_request(RequestMethod::DELETE, path, parameters, access_level).await?;
self.deserialize_response(&body)
}
}
impl Auth for GmoApi {
fn require_api_key(&self) -> Result<&str> {
match &self.api_key {
Some(key) => Ok(key),
None => Err(anyhow::anyhow!("api_key is required")),
}
}
fn require_api_secret(&self) -> Result<&str> {
match &self.api_secret {
Some(secret) => Ok(secret),
None => Err(anyhow::anyhow!("api_secret is required")),
}
}
fn create_signature<T: Serialize>(&self, timestamp: u64, method: RequestMethod, path: &str, parameters: Option<T>) -> Result<String> {
let api_secret = self.require_api_secret()?;
let message = match method {
RequestMethod::GET => format!("{}{}{}", timestamp, method, path),
RequestMethod::POST => {
let parameters_json = serde_json::to_string(¶meters)?;
format!("{}{}{}{}", timestamp, method, path, parameters_json)
},
RequestMethod::PUT => format!("{}{}{}", timestamp, method, path),
RequestMethod::DELETE => format!("{}{}{}", timestamp, method, path),
};
let signed_key = hmac::Key::new(hmac::HMAC_SHA256, api_secret.as_bytes());
let signature = hex::encode(hmac::sign(&signed_key, message.as_bytes()).as_ref());
Ok(signature)
}
}
impl Request for GmoApi {
fn build_headers<T: Serialize>(&self, timestamp: u64, method: RequestMethod, path: &str, parameters: Option<T>) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert("API-KEY", HeaderValue::from_str(self.require_api_key()?)?);
headers.insert("API-TIMESTAMP", HeaderValue::from_str(×tamp.to_string())?);
headers.insert("API-SIGN", HeaderValue::from_str(&self.create_signature(timestamp, method, path, parameters)?)?);
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
Ok(headers)
}
async fn send_request<T: Serialize + Clone>(&self, method: RequestMethod, path: &str, parameters: Option<T>, access_level: AccessLevel) -> Result<String> {
let url = format!("{}{}{}", self.base_url(), access_level, path);
let timestamp = utils::get_timestamp()?;
let client = reqwest::Client::new();
let response = match method {
RequestMethod::GET => {
match access_level {
AccessLevel::Public => {
match parameters {
Some(parameters) => client.get(&url).query(¶meters).send().await?,
None => client.get(&url).send().await?,
}
},
AccessLevel::Private => {
let headers = self.build_headers(timestamp, method, path, parameters.clone())?;
match parameters {
Some(parameters) => client.get(&url).headers(headers).query(¶meters).send().await?,
None => client.get(&url).headers(headers).send().await?,
}
},
}
},
RequestMethod::POST => {
let headers = self.build_headers(timestamp, method, path, parameters.clone())?;
client.post(&url).headers(headers).json(¶meters).send().await?
},
RequestMethod::PUT => {
let headers = self.build_headers(timestamp, method, path, parameters.clone())?;
client.put(&url).headers(headers).json(¶meters).send().await?
},
RequestMethod::DELETE => {
let headers = self.build_headers(timestamp, method, path, parameters.clone())?;
client.delete(&url).headers(headers).json(¶meters).send().await?
},
};
match response.status().as_u16() {
200 => {
let body = response.text().await?;
Ok(body)
},
_ => {
Err(anyhow!(format!(
"Request error status code {}: response details: {:?}",
response.status().as_u16(),
response.text().await?
)))
},
}
}
fn deserialize_response<P: serde::de::DeserializeOwned>(&self, body: &str) -> Result<P> {
let response: Value = serde_json::from_str(body)?;
let status = response.get("status").and_then(Value::as_i64);
match status {
Some(status) => {
match status {
0 => {
let response: P = serde_json::from_str(body)?;
Ok(response)
},
_ => Err(anyhow!("Response error: {}", response))
}
},
None => {
Err(anyhow!("Response error {}", response))
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Pagination {
current_page: i32,
count: i32,
}
impl Pagination {
pub fn current_page(&self) -> i32 {
self.current_page
}
pub fn count(&self) -> i32 {
self.count
}
}