use std::time::Duration;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::common::{
catch_not_found, parse_data_envelope, to_safe_id, PaginationList, QueryParams,
};
use crate::error::ApifyClientResult;
use crate::http_client::{HttpClient, HttpMethod, HttpRequest};
const WAIT_FOR_FINISH_POLL_INTERVAL: Duration = Duration::from_millis(250);
const WAIT_FOR_FINISH_REQUEST_SECS: i64 = 60;
pub(crate) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(360);
pub(crate) const SMALL_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const MEDIUM_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub(crate) struct ResourceContext {
pub(crate) http: HttpClient,
pub(crate) url: String,
pub(crate) base_params: QueryParams,
pub(crate) api_origin: String,
pub(crate) public_origin: String,
}
impl ResourceContext {
pub(crate) fn collection(http: HttpClient, base_url: &str, resource_path: &str) -> Self {
Self::new(http, format!("{base_url}/{resource_path}"), base_url)
}
pub(crate) fn single(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
Self::new(
http,
format!("{base_url}/{resource_path}/{}", to_safe_id(id)),
base_url,
)
}
fn new(http: HttpClient, url: String, base_url: &str) -> Self {
let api_origin = origin_of(base_url);
Self {
http,
url,
base_params: QueryParams::new(),
public_origin: api_origin.clone(),
api_origin,
}
}
pub(crate) fn with_public_origin(mut self, public_origin: &str) -> Self {
self.public_origin = origin_of(public_origin);
self
}
pub(crate) fn url(&self, sub_path: Option<&str>) -> String {
match sub_path {
Some(path) => format!("{}/{path}", self.url),
None => self.url.clone(),
}
}
pub(crate) fn public_url(&self, sub_path: Option<&str>) -> String {
let api_url = self.url(sub_path);
if self.public_origin == self.api_origin {
return api_url;
}
match api_url.strip_prefix(&self.api_origin) {
Some(rest) => format!("{}{rest}", self.public_origin),
None => api_url,
}
}
pub(crate) fn merged_params(&self, params: &QueryParams) -> QueryParams {
let mut merged = self.base_params.clone();
merged.extend(params);
merged
}
}
fn origin_of(url: &str) -> String {
match url.split_once("://") {
Some((scheme, rest)) => {
let host = rest.split('/').next().unwrap_or(rest);
format!("{scheme}://{host}")
}
None => url.split('/').next().unwrap_or(url).to_string(),
}
}
impl QueryParams {
pub(crate) fn extend(&mut self, other: &QueryParams) {
for (k, v) in other.pairs_ref() {
self.push_raw(k.clone(), v.clone());
}
}
}
pub(crate) async fn get_resource<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
) -> ApifyClientResult<Option<T>> {
let result = get_resource_required(ctx, sub_path, params).await;
catch_not_found(result)
}
pub(crate) async fn get_resource_required<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
) -> ApifyClientResult<T> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let response = ctx
.http
.call(HttpRequest {
method: HttpMethod::Get,
url,
headers: Default::default(),
body: None,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn update_resource<B: Serialize, T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
body: &B,
) -> ApifyClientResult<T> {
let url = ctx
.merged_params(&QueryParams::new())
.apply_to_url(&ctx.url(sub_path));
let body_bytes = serde_json::to_vec(body)?;
let mut headers = std::collections::HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = ctx
.http
.call(HttpRequest {
method: HttpMethod::Put,
url,
headers,
body: Some(body_bytes),
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn delete_resource(
ctx: &ResourceContext,
sub_path: Option<&str>,
) -> ApifyClientResult<()> {
let url = ctx
.merged_params(&QueryParams::new())
.apply_to_url(&ctx.url(sub_path));
let result = ctx
.http
.call(HttpRequest {
method: HttpMethod::Delete,
url,
headers: Default::default(),
body: None,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await;
catch_not_found(result.map(|_| ()))?;
Ok(())
}
pub(crate) async fn list_resource<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
) -> ApifyClientResult<PaginationList<T>> {
get_resource_required(ctx, sub_path, params).await
}
pub(crate) async fn create_resource<B: Serialize, T: DeserializeOwned>(
ctx: &ResourceContext,
params: &QueryParams,
body: &B,
) -> ApifyClientResult<T> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(None));
let body_bytes = serde_json::to_vec(body)?;
let mut headers = std::collections::HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = ctx
.http
.call(HttpRequest {
method: HttpMethod::Post,
url,
headers,
body: Some(body_bytes),
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn get_or_create_named<T: DeserializeOwned>(
ctx: &ResourceContext,
name: Option<&str>,
) -> ApifyClientResult<T> {
let mut params = QueryParams::new();
params.add_str("name", name.map(|s| s.to_string()));
let url = params.apply_to_url(&ctx.url(None));
let response = ctx
.http
.call(HttpRequest {
method: HttpMethod::Post,
url,
headers: Default::default(),
body: None,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn post_action<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: Option<Vec<u8>>,
content_type: Option<&str>,
) -> ApifyClientResult<T> {
let response = post_send(ctx, sub_path, params, body, content_type).await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn post_action_raw<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: Option<Vec<u8>>,
content_type: Option<&str>,
) -> ApifyClientResult<T> {
let response = post_send(ctx, sub_path, params, body, content_type).await?;
Ok(serde_json::from_slice(&response.body)?)
}
async fn post_send(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: Option<Vec<u8>>,
content_type: Option<&str>,
) -> ApifyClientResult<crate::http_client::HttpResponse> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let mut headers = std::collections::HashMap::new();
if let Some(ct) = content_type {
headers.insert("Content-Type".to_string(), ct.to_string());
}
ctx.http
.call(HttpRequest {
method: HttpMethod::Post,
url,
headers,
body,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await
}
pub(crate) async fn get_raw(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
) -> ApifyClientResult<Option<crate::http_client::HttpResponse>> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let result = ctx
.http
.call(HttpRequest {
method: HttpMethod::Get,
url,
headers: Default::default(),
body: None,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await;
catch_not_found(result)
}
pub(crate) async fn head_exists(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
) -> ApifyClientResult<bool> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let result = ctx
.http
.call(HttpRequest {
method: HttpMethod::Head,
url,
headers: Default::default(),
body: None,
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await;
Ok(catch_not_found(result)?.is_some())
}
pub(crate) async fn put_raw(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: Vec<u8>,
content_type: &str,
) -> ApifyClientResult<()> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let mut headers = std::collections::HashMap::new();
headers.insert("Content-Type".to_string(), content_type.to_string());
ctx.http
.call(HttpRequest {
method: HttpMethod::Put,
url,
headers,
body: Some(body),
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
Ok(())
}
pub(crate) async fn post_with_body<T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: Option<Vec<u8>>,
content_type: &str,
) -> ApifyClientResult<T> {
post_action(ctx, sub_path, params, body, Some(content_type)).await
}
pub(crate) async fn delete_with_body<B: Serialize, T: DeserializeOwned>(
ctx: &ResourceContext,
sub_path: Option<&str>,
params: &QueryParams,
body: &B,
) -> ApifyClientResult<T> {
let url = ctx.merged_params(params).apply_to_url(&ctx.url(sub_path));
let body_bytes = serde_json::to_vec(body)?;
let mut headers = std::collections::HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = ctx
.http
.call(HttpRequest {
method: HttpMethod::Delete,
url,
headers,
body: Some(body_bytes),
timeout: DEFAULT_REQUEST_TIMEOUT,
})
.await?;
parse_data_envelope(&response.body)
}
pub(crate) async fn wait_for_finish<T, F>(
ctx: &ResourceContext,
wait_secs: Option<i64>,
is_terminal: F,
) -> ApifyClientResult<T>
where
T: DeserializeOwned,
F: Fn(&T) -> bool,
{
let start = std::time::Instant::now();
let budget = wait_secs.map(|s| Duration::from_secs(s.max(0) as u64));
loop {
let remaining_request_secs = match budget {
Some(budget) => {
let elapsed = start.elapsed();
if elapsed >= budget {
let mut params = QueryParams::new();
params.add_int("waitForFinish", Some(0));
return get_resource_required(ctx, None, ¶ms).await;
}
let remaining = (budget - elapsed).as_secs() as i64;
remaining.min(WAIT_FOR_FINISH_REQUEST_SECS)
}
None => WAIT_FOR_FINISH_REQUEST_SECS,
};
let mut params = QueryParams::new();
params.add_int("waitForFinish", Some(remaining_request_secs));
let resource: Option<T> = get_resource(ctx, None, ¶ms).await?;
if let Some(resource) = resource {
if is_terminal(&resource) {
return Ok(resource);
}
if let Some(budget) = budget {
if start.elapsed() >= budget {
return Ok(resource);
}
}
}
crate::http_client::sleep_public(WAIT_FOR_FINISH_POLL_INTERVAL).await;
}
}