use crate::apikey::ApiKey;
use crate::client::Config;
use crate::error::{RestError, Result};
use crate::response::Response;
use crate::token::Token;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
const REST_TIMEOUT: Duration = Duration::from_secs(300);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone)]
pub struct RestContext {
config: Config,
token: Arc<Mutex<Option<Token>>>,
api_key: Option<ApiKey>,
}
impl RestContext {
pub fn new() -> Self {
RestContext {
config: Config::default(),
token: Arc::new(Mutex::new(None)),
api_key: None,
}
}
pub fn with_config(config: Config) -> Self {
RestContext {
config,
token: Arc::new(Mutex::new(None)),
api_key: None,
}
}
pub fn with_token(self, token: Token) -> Self {
*self.token.lock().unwrap() = Some(token);
self
}
pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
self.api_key = Some(api_key);
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.config.set_debug(debug);
self
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn apply<T, P>(&self, path: &str, method: &str, param: P) -> Result<T>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let response = self.do_request(path, method, param)?;
response.apply()
}
pub fn do_request<P>(&self, path: &str, method: &str, param: P) -> Result<Response>
where
P: Serialize,
{
let param_json = serde_json::to_value(param)?;
self.request_inner(path, method, ¶m_json, true)
}
fn request_inner(
&self,
path: &str,
method: &str,
param_json: &serde_json::Value,
allow_renew: bool,
) -> Result<Response> {
let base_url = self.config.base_url();
let url = format!("{}/_special/rest/{}", base_url, path);
let mut query_params: HashMap<String, String> = HashMap::new();
let mut body_bytes: Vec<u8> = Vec::new();
match method {
"GET" | "HEAD" | "OPTIONS" => {
let param_str = serde_json::to_string(param_json)?;
query_params.insert("_".to_string(), param_str);
}
"PUT" | "POST" | "PATCH" => {
body_bytes = serde_json::to_vec(param_json)?;
}
"DELETE" => {
}
_ => {
return Err(RestError::RequestBuild(format!(
"Unsupported HTTP method: {}",
method
)))
}
}
if let Some(ref api_key) = self.api_key {
api_key.apply_params(method, path, &mut query_params, &body_bytes)?;
}
let full_url = if query_params.is_empty() {
url
} else {
let query = form_urlencoded::Serializer::new(String::new())
.extend_pairs(query_params.iter())
.finish();
format!("{}?{}", url, query)
};
let current_token = if self.api_key.is_none() {
self.token.lock().unwrap().clone()
} else {
None
};
let mut request = rsurl::Request::new(method, &full_url)?
.header("Sec-Rest-Http", "false")
.max_time(REST_TIMEOUT)
.connect_timeout(CONNECT_TIMEOUT);
if let Some(ref token) = current_token {
request = request.header("Authorization", &format!("Bearer {}", token.access_token));
}
if !body_bytes.is_empty() {
request = request
.header("Content-Type", "application/json")
.body(body_bytes);
}
let start = std::time::Instant::now();
let http_response = request.send()?;
let status = http_response.status;
let request_id = http_response.header("X-Request-Id").map(|s| s.to_string());
let body = http_response.body;
if self.config.debug() {
let duration = start.elapsed();
eprintln!(
"[rest] {} {} => {:?} (status: {})",
method, path, duration, status
);
}
let mut response: Response = serde_json::from_slice(&body).map_err(|e| {
if !(200..400).contains(&status) {
RestError::http(
status,
String::from_utf8_lossy(&body).to_string(),
Some(Box::new(e)),
)
} else {
RestError::Json(e)
}
})?;
response.request_id = request_id;
if allow_renew {
if let Some(token) = current_token {
if response.token.as_deref() == Some("invalid_request_token")
&& response.extra.as_deref() == Some("token_expired")
{
if self.config.debug() {
eprintln!("[rest] Token expired, attempting renewal");
}
let renewed = self.renew_token(&token)?;
*self.token.lock().unwrap() = Some(renewed);
return self.request_inner(path, method, param_json, false);
}
}
}
if response.result == "redirect" {
if response.exception.as_deref() == Some("Exception\\Login") {
return Err(RestError::LoginRequired);
}
return Err(RestError::from_response(response));
}
if response.result == "error" {
return Err(RestError::from_response(response));
}
Ok(response)
}
fn renew_token(&self, token: &Token) -> Result<Token> {
if !token.has_client_id() {
return Err(RestError::NoClientId);
}
if !token.has_refresh_token() {
return Err(RestError::NoRefreshToken);
}
let ctx = RestContext {
config: self.config.clone(),
token: Arc::new(Mutex::new(None)),
api_key: None,
};
let mut params = HashMap::new();
params.insert("grant_type", "refresh_token");
params.insert("client_id", &token.client_id);
params.insert("refresh_token", &token.refresh_token);
params.insert("noraw", "true");
let mut renewed: Token = ctx.apply("OAuth2:token", "POST", params)?;
renewed.client_id = token.client_id.clone();
Ok(renewed)
}
}
impl Default for RestContext {
fn default() -> Self {
Self::new()
}
}
pub fn apply<T, P>(path: &str, method: &str, param: P) -> Result<T>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
RestContext::new().apply(path, method, param)
}
pub fn do_request<P>(path: &str, method: &str, param: P) -> Result<Response>
where
P: Serialize,
{
RestContext::new().do_request(path, method, param)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rest_context_creation() {
let ctx = RestContext::new();
assert_eq!(ctx.config().scheme(), "https");
assert_eq!(ctx.config().host(), "www.atonline.com");
}
#[test]
fn test_rest_context_with_config() {
let config = Config::new("http".to_string(), "localhost:8080".to_string());
let ctx = RestContext::with_config(config);
assert_eq!(ctx.config().scheme(), "http");
assert_eq!(ctx.config().host(), "localhost:8080");
}
}