#[cfg(feature = "decisions")]
use crate::decisions::{
self, Decision, DecisionPage, DecisionRequest, DecisionResult, DecisionStatus, Decisions,
Entity, EntityType,
};
#[cfg(feature = "labels")]
use crate::labels::{LabelOptions, LabelProperties};
#[cfg(feature = "score")]
use crate::score::{ScoreOptions, ScoreQueryParams};
#[cfg(feature = "verification")]
use crate::verification::{
self, CheckOptions, CheckRequest, CheckResponse, ResendRequest, SendRequest, SendResponse,
};
#[cfg(feature = "webhooks")]
use crate::webhooks::{self, Webhook, WebhookRequest, WebhookResponse, WebhooksResponse};
use crate::{
common::{abuse_type_serialize, AbuseType},
events::{self, Event, EventOptions, EventQueryParams, EventResponse, ScoreResponse, Scores},
Error, Result,
};
use async_trait::async_trait;
#[cfg(any(feature = "awc", feature = "awc3", feature = "reqwest"))]
use futures::future::TryFutureExt;
use serde::Serialize;
use std::borrow::Cow;
use std::fmt;
use std::time::Duration;
use tracing::{debug, instrument, trace};
const SIFT_ORIGIN: &str = "https://api.sift.com";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
pub struct Client<T> {
pub api_key: String,
pub account_id: Option<String>,
pub http_client: T,
pub origin: String,
}
impl<T: Clone> Clone for Client<T> {
fn clone(&self) -> Self {
Client {
api_key: self.api_key.clone(),
account_id: None,
http_client: self.http_client.clone(),
origin: self.origin.clone(),
}
}
}
impl<T: HttpClient> Client<T> {
pub fn new(api_key: impl Into<String>, http_client: T) -> Self {
Client {
api_key: api_key.into(),
account_id: None,
http_client,
origin: SIFT_ORIGIN.into(),
}
}
pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
self.origin = origin.into();
self
}
pub fn with_account_id(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
#[instrument(skip(self, event, options))]
pub async fn track(&self, event: Event, options: EventOptions) -> Result<Option<Scores>> {
let version = options.version.unwrap_or(events::ApiVersion::V205);
let path = options.path.clone().unwrap_or(Cow::Borrowed("events"));
let timeout = options.timeout.unwrap_or(DEFAULT_TIMEOUT);
let url = format!("{}/{}/{}", self.origin, version, path);
let mut body = serde_json::json!(&event);
body["$api_key"] = serde_json::json!(options.api_key.as_deref().unwrap_or(&self.api_key));
trace!(?event, ?options, "preparing event");
let query_params = EventQueryParams::from(options);
debug!(
?url,
query_params = ?serde_urlencoded::to_string(&query_params),
body = ?serde_json::to_string(&body),
"tracking event"
);
let sift_response = self
.http_client
.post(&url, Some(&query_params.into()), Some(&body), timeout, None)
.await?;
if sift_response.is_none() {
return Ok(None);
}
let event_json = sift_response.unwrap();
trace!(?event_json, "sift event API response");
match serde_json::from_value(event_json)? {
EventResponse {
score_response:
Some(ScoreResponse {
scores: Some(scores),
..
}),
..
} => Ok(Some(scores)),
EventResponse {
status,
error_message,
..
}
| EventResponse {
score_response:
Some(ScoreResponse {
status,
error_message,
..
}),
..
} if status != 0 => Err(Error::Request {
status,
error_message,
}),
_ => Ok(None),
}
}
#[cfg(feature = "score")]
#[instrument(skip(self, opts))]
pub async fn get_user_score<U>(
&self,
user_id: U,
mut opts: ScoreOptions,
) -> Result<ScoreResponse>
where
U: AsRef<str> + fmt::Debug,
{
let version = opts.version.unwrap_or(events::ApiVersion::V205);
let path_prefix = opts.path_prefix.unwrap_or("users");
let path_suffix = opts.path_prefix.unwrap_or("score");
let timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
let user_id = urlencoding::encode(user_id.as_ref()).to_string();
let url = format!(
"{}/{}/{}/{}/{}",
self.origin, version, path_prefix, user_id, path_suffix
);
opts.api_key.get_or_insert_with(|| self.api_key.clone());
let query_params = ScoreQueryParams::from(opts);
debug!(?url, query_params = ?serde_urlencoded::to_string(&query_params), "retrieving score");
let score_json = self
.http_client
.get(&url, &query_params.into(), timeout, None)
.await?;
trace!(?score_json, "sift score API response");
let score_response = serde_json::from_value(score_json)?;
Ok(score_response)
}
#[cfg(feature = "score")]
#[instrument(skip(self, opts))]
pub async fn rescore_user<U>(&self, user_id: U, mut opts: ScoreOptions) -> Result<ScoreResponse>
where
U: AsRef<str> + fmt::Debug,
{
let version = opts.version.unwrap_or(events::ApiVersion::V205);
let path_prefix = opts.path_prefix.unwrap_or("users");
let path_suffix = opts.path_prefix.unwrap_or("score");
let timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
let user_id = urlencoding::encode(user_id.as_ref()).to_string();
let url = format!(
"{}/{}/{}/{}/{}",
self.origin, version, path_prefix, user_id, path_suffix
);
opts.api_key.get_or_insert_with(|| self.api_key.clone());
let query_params = ScoreQueryParams::from(opts);
debug!(?url, query_params = ?serde_urlencoded::to_string(&query_params), "rescoring");
let score_json = self
.http_client
.post(&url, Some(&query_params.into()), None, timeout, None)
.await?;
trace!(?score_json, "sift score API response");
match score_json {
Some(score_json) => {
let score_response = serde_json::from_value(score_json)?;
Ok(score_response)
}
None => Err(Error::Server(
"Expected a score, but received empty server response".into(),
)),
}
}
#[cfg(feature = "labels")]
#[instrument(skip(self, properties, opts))]
pub async fn label<U>(
&self,
user_id: U,
properties: LabelProperties,
opts: LabelOptions,
) -> Result<()>
where
U: AsRef<str> + fmt::Debug,
{
let formatted_id = urlencoding::encode(user_id.as_ref()).to_string();
self.track(properties.into(), (opts, formatted_id.as_str()).into())
.await?;
Ok(())
}
#[cfg(feature = "verification")]
#[instrument(skip(self, req))]
pub async fn send_verification(&self, req: SendRequest) -> Result<SendResponse> {
let timeout = DEFAULT_TIMEOUT;
let api_version = verification::ApiVersion::V1;
let url = format!("{}/{}/verification/send", self.origin, api_version);
let body = serde_json::json!(req);
let auth = Some(self.api_key.as_str());
debug!(?url, ?req, "sending verification");
trace!(body = ?serde_json::to_string(&body), "verification data");
let response_json = self
.http_client
.post(&url, None, Some(&body), timeout, auth)
.await?;
trace!(?response_json, "sift verification API response");
match response_json {
Some(response_json) => match serde_json::from_value(response_json)? {
SendResponse {
status,
error_message,
..
} if status != 0 => {
tracing::warn!(status, ?error_message, "verification send error");
Err(Error::Request {
status,
error_message,
})
}
send_success => {
debug!(?send_success, "verification send success");
Ok(send_success)
}
},
None => Err(Error::Server(
"Expected a verification, but received empty server response".into(),
)),
}
}
#[cfg(feature = "verification")]
#[instrument(skip(self, req))]
pub async fn resend_verification(&self, req: ResendRequest) -> Result<SendResponse> {
let timeout = DEFAULT_TIMEOUT;
let api_version = verification::ApiVersion::V1;
let url = format!("{}/{}/verification/resend", self.origin, api_version);
let body = serde_json::json!(req);
let auth = Some(self.api_key.as_str());
debug!(?url, ?req, "resending verification");
trace!(body = ?serde_json::to_string(&body), "verification data");
let response_json = self
.http_client
.post(&url, None, Some(&body), timeout, auth)
.await?;
trace!(?response_json, "sift verification API response");
match response_json {
Some(response_json) => match serde_json::from_value(response_json)? {
SendResponse {
status,
error_message,
..
} if status != 0 => {
tracing::warn!(status, ?error_message, "verification resend error");
Err(Error::Request {
status,
error_message,
})
}
resend_success => {
debug!(?resend_success, "verification resend success");
Ok(resend_success)
}
},
None => Err(Error::Server(
"Expected a verification, but received empty server response".into(),
)),
}
}
#[cfg(feature = "verification")]
#[instrument(skip(self, code, opts))]
pub async fn check_verification<U>(
&self,
user_id: U,
code: String,
opts: CheckOptions,
) -> Result<CheckResponse>
where
U: Into<String> + fmt::Debug,
{
let CheckOptions {
verified_event,
verified_entity_id,
timeout,
version,
} = opts;
let req = CheckRequest {
user_id: user_id.into(),
code,
verified_event,
verified_entity_id,
};
let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
let api_version = version.unwrap_or(verification::ApiVersion::V1);
let url = format!("{}/{}/verification/check", self.origin, api_version);
let body = serde_json::json!(req);
let auth = Some(self.api_key.as_str());
debug!(?url, ?req, "checking verification");
let response_json = self
.http_client
.post(&url, None, Some(&body), timeout, auth)
.await?;
trace!(?response_json, "sift verification API response");
match response_json {
Some(response_json) => match serde_json::from_value(response_json)? {
CheckResponse {
status,
error_message,
..
} if status != 0 => {
tracing::warn!(status, ?error_message, "verification check error");
Err(Error::Request {
status,
error_message,
})
}
check_success => {
debug!(?check_success, "verification check success");
Ok(check_success)
}
},
None => Err(Error::Server(
"Expected a verification, but received empty server response".into(),
)),
}
}
#[cfg(feature = "webhooks")]
#[instrument(skip(self, req))]
pub async fn create_webhook(&self, req: WebhookRequest) -> Result<Webhook> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = webhooks::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/webhooks",
self.origin, api_version, account_id
);
let body = serde_json::json!(req);
let auth = Some(self.api_key.as_str());
debug!(?url, ?req, "creating webhook");
trace!(body = ?serde_json::to_string(&body), "webhook data");
let response_json = self
.http_client
.post(&url, None, Some(&body), timeout, auth)
.await?;
trace!(?response_json, "sift webhook API response");
match response_json {
Some(response_json) => Ok(serde_json::from_value(response_json)?),
None => Err(Error::Server(
"Expected a webhook, but received empty server response".into(),
)),
}
}
#[cfg(feature = "webhooks")]
#[instrument(skip(self))]
pub async fn get_webhooks(&self) -> Result<Vec<Webhook>> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = webhooks::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/webhooks",
self.origin, api_version, account_id,
);
let auth = Some(self.api_key.as_str());
debug!(?url, "Retrieving webhooks");
let response_json = self
.http_client
.get(&url, &QueryParams::default(), timeout, auth)
.await?;
trace!(body = ?serde_json::to_string(&response_json), "sift webhook API response");
match serde_json::from_value(response_json)? {
WebhooksResponse::Webhooks { data } => Ok(data),
WebhooksResponse::Error(err) => Err(err),
}
}
#[cfg(feature = "webhooks")]
#[instrument(skip(self, id))]
pub async fn get_webhook(&self, id: u64) -> Result<Webhook> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = webhooks::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/webhooks/{}",
self.origin, api_version, account_id, id
);
let auth = Some(self.api_key.as_str());
debug!(?url, "Retrieving webhook");
let response_json = self
.http_client
.get(&url, &QueryParams::default(), timeout, auth)
.await?;
trace!(?response_json, "sift webhook API response");
match serde_json::from_value(response_json)? {
WebhookResponse::Webhook(webhook) => Ok(webhook),
WebhookResponse::Error(err) => Err(err),
}
}
#[cfg(feature = "webhooks")]
#[instrument(skip(self, webhook))]
pub async fn update_webhook(&self, webhook: Webhook) -> Result<Webhook> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = webhooks::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/webhooks/{}",
self.origin, api_version, account_id, webhook.id,
);
let body = serde_json::json!(webhook);
let auth = self.api_key.as_str();
debug!(?url, "updating webhook");
trace!(body = ?serde_json::to_string(&body), "webhook data");
let response_json = self.http_client.put(&url, &body, timeout, auth).await?;
trace!(?response_json, "sift webhook update response");
match serde_json::from_value(response_json)? {
WebhookResponse::Webhook(webhook) => Ok(webhook),
WebhookResponse::Error(err) => Err(err),
}
}
#[cfg(feature = "webhooks")]
#[instrument(skip(self))]
pub async fn delete_webhook(&self, id: u64) -> Result<()> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = webhooks::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/webhooks/{}",
self.origin, api_version, account_id, id,
);
let auth = self.api_key.as_str();
debug!(?url, "deleting webhook");
self.http_client.delete(&url, timeout, auth).await
}
pub fn verify_webhook_signature(
&self,
signature: &str,
body: &[u8],
webhook_secret: &str,
) -> Result<()> {
use hmac::{Hmac, Mac};
match signature.split_once('=') {
Some(("sha1", tag)) if tag.is_ascii() && tag.len() == 40 => {
let mut mac = Hmac::<sha1::Sha1>::new_from_slice(webhook_secret.as_bytes())
.map_err(|err| Error::Server(err.to_string()))?;
mac.update(body);
let hex = (0..20).fold([0; 20], |mut acc, i| {
acc[i] = u8::from_str_radix(&tag[(i * 2)..=(i * 2 + 1)], 16).unwrap_or(0);
acc
});
mac.verify_slice(&hex)
.map_err(|err| Error::Server(err.to_string()))
}
Some((alg, _)) => Err(Error::Server(format!("unsupported type: {}", alg))),
None => Err(Error::Server("Invalid signature value".into())),
}
}
#[cfg(feature = "decisions")]
#[instrument(skip(self, entity, decision))]
pub async fn apply_decision(
&self,
entity: Entity,
decision: DecisionRequest,
) -> Result<Decision> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = decisions::ApiVersion::V3;
let url = format!(
"{}/{}/accounts/{}/{}/decisions",
self.origin, api_version, account_id, entity,
);
let body = serde_json::json!(&decision);
let auth = Some(self.api_key.as_str());
debug!(?url, ?decision, "applying decision");
trace!(body = ?serde_json::to_string(&body), "decision data");
let response_json = self
.http_client
.post(&url, None, Some(&body), timeout, auth)
.await?;
trace!(?response_json, "decision response");
match response_json {
Some(response_json) => match serde_json::from_value(response_json)? {
DecisionResult::Decision(decision) => Ok(decision),
DecisionResult::Error(err) => Err(err),
},
None => Err(Error::Server(
"Expected a decision, but received empty server response".into(),
)),
}
}
#[cfg(feature = "decisions")]
#[instrument(skip(self, entity))]
pub async fn decision_status(&self, entity: Entity) -> Result<Decisions> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = decisions::ApiVersion::V3;
let path = if let Entity::Order { order_id, .. } = entity {
format!("orders/{}", order_id)
} else {
format!("{}", entity)
};
let url = format!(
"{}/{}/accounts/{}/{}/decisions",
self.origin, api_version, account_id, path,
);
let auth = Some(self.api_key.as_str());
debug!(?url, "getting decision status");
let response_json = self
.http_client
.get(&url, &QueryParams::default(), timeout, auth)
.await?;
trace!(?response_json, "decision status response");
match serde_json::from_value(response_json)? {
DecisionResult::Decision(DecisionStatus { decisions }) => Ok(decisions),
DecisionResult::Error(err) => Err(err),
}
}
#[cfg(feature = "decisions")]
#[instrument(skip(self, entity_type, abuse_types, limit, offset))]
pub async fn get_decisions(
&self,
entity_type: Option<EntityType>,
abuse_types: Option<Vec<AbuseType>>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<DecisionPage> {
let account_id = self
.account_id
.as_ref()
.ok_or_else(|| Error::Server("account id not specified".into()))?;
let timeout = DEFAULT_TIMEOUT;
let api_version = decisions::ApiVersion::V3;
let query_params = QueryParams {
entity_type,
abuse_types,
limit,
from: offset,
..Default::default()
};
let url = format!(
"{}/{}/accounts/{}/decisions",
self.origin, api_version, account_id
);
let auth = Some(self.api_key.as_str());
debug!(
?url,
query_params = ?serde_urlencoded::to_string(&query_params),
"getting decision page"
);
let response_json = self
.http_client
.get(&url, &QueryParams::default(), timeout, auth)
.await?;
trace!(
json = %serde_json::to_string(&response_json).unwrap(),
"decision page response"
);
match serde_json::from_value(response_json)? {
DecisionResult::Decision(page) => Ok(page),
DecisionResult::Error(err) => Err(err),
}
}
}
impl<T: HttpClient + Default> Client<T> {
pub fn with_api_key(api_key: impl Into<String>) -> Self {
Client {
api_key: api_key.into(),
account_id: None,
http_client: Default::default(),
origin: SIFT_ORIGIN.into(),
}
}
}
impl<T> fmt::Debug for Client<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("api_key", &"****")
.field("account_id", &self.account_id)
.field("origin", &self.origin)
.finish()
}
}
#[derive(Default, Debug, Serialize)]
pub struct QueryParams {
api_key: Option<String>,
return_score: Option<bool>,
#[cfg(feature = "decisions")]
entity_type: Option<EntityType>,
#[serde(serialize_with = "abuse_type_serialize")]
abuse_types: Option<Vec<AbuseType>>,
return_action: Option<bool>,
return_workflow_status: Option<bool>,
limit: Option<u32>,
from: Option<u32>,
}
impl From<EventQueryParams> for QueryParams {
fn from(eqp: EventQueryParams) -> Self {
let EventQueryParams {
return_score,
abuse_types,
return_action,
return_workflow_status,
} = eqp;
QueryParams {
return_score,
abuse_types,
return_action,
return_workflow_status,
..Default::default()
}
}
}
#[cfg(feature = "score")]
impl From<ScoreQueryParams> for QueryParams {
fn from(sqp: ScoreQueryParams) -> Self {
let ScoreQueryParams {
api_key,
abuse_types,
} = sqp;
QueryParams {
api_key: Some(api_key),
abuse_types,
..Default::default()
}
}
}
#[async_trait(?Send)]
pub trait HttpClient {
async fn get(
&self,
url: &str,
query_params: &QueryParams,
timeout: Duration,
username: Option<&str>,
) -> Result<serde_json::Value>;
async fn post(
&self,
url: &str,
query_params: Option<&QueryParams>,
body: Option<&serde_json::Value>,
timeout: Duration,
username: Option<&str>,
) -> Result<Option<serde_json::Value>>;
async fn put(
&self,
url: &str,
body: &serde_json::Value,
timeout: Duration,
username: &str,
) -> Result<serde_json::Value>;
async fn delete(&self, url: &str, timeout: Duration, username: &str) -> Result<()>;
}
#[cfg(feature = "awc3")]
#[async_trait(?Send)]
impl HttpClient for awc3::Client {
async fn get(
&self,
url: &str,
query_params: &QueryParams,
timeout: Duration,
username: Option<&str>,
) -> Result<serde_json::Value> {
let mut req = self
.get(url)
.insert_header((
awc3::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
))
.timeout(timeout)
.query(&query_params)
.map_err(|err| Error::Server(err.to_string()))?;
if let Some(username) = username {
req = req.basic_auth(username, "");
}
let mut res = req
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn post(
&self,
url: &str,
query_params: Option<&QueryParams>,
body: Option<&serde_json::Value>,
timeout: Duration,
username: Option<&str>,
) -> Result<Option<serde_json::Value>> {
let mut req = self
.post(url)
.insert_header((
awc3::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
))
.timeout(timeout);
if let Some(username) = username {
req = req.basic_auth(username, "");
}
if let Some(query_params) = query_params {
req = req
.query(&query_params)
.map_err(|err| Error::Server(err.to_string()))?;
}
let mut res = if let Some(body) = body {
req.send_json(&body)
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?
} else {
req.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?
};
if res.status() == awc3::http::StatusCode::NO_CONTENT {
return Ok(None);
} else if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.map_ok(Some)
.await
}
async fn put(
&self,
url: &str,
body: &serde_json::Value,
timeout: Duration,
username: &str,
) -> Result<serde_json::Value> {
let mut res = self
.put(url)
.insert_header((
awc3::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
))
.basic_auth(username, "")
.timeout(timeout)
.send_json(&body)
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn delete(&self, url: &str, timeout: Duration, username: &str) -> Result<()> {
let mut res = self
.delete(url)
.insert_header((
awc3::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
))
.basic_auth(username, "")
.timeout(timeout)
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
Ok(())
}
}
#[cfg(feature = "awc3")]
pub type Awc3Client = Client<awc3::Client>;
#[cfg(feature = "awc")]
#[async_trait(?Send)]
impl HttpClient for awc::Client {
async fn get(
&self,
url: &str,
query_params: &QueryParams,
timeout: Duration,
username: Option<&str>,
) -> Result<serde_json::Value> {
let mut req = self
.get(url)
.header(
awc::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.timeout(timeout)
.query(&query_params)
.map_err(|err| Error::Server(err.to_string()))?;
if let Some(username) = username {
req = req.basic_auth(username, None);
}
let mut res = req
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn post(
&self,
url: &str,
query_params: Option<&QueryParams>,
body: Option<&serde_json::Value>,
timeout: Duration,
username: Option<&str>,
) -> Result<Option<serde_json::Value>> {
let mut req = self
.post(url)
.header(
awc::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.timeout(timeout);
if let Some(username) = username {
req = req.basic_auth(username, None);
}
if let Some(query_params) = query_params {
req = req
.query(&query_params)
.map_err(|err| Error::Server(err.to_string()))?;
}
let mut res = if let Some(body) = body {
req.send_json(&body)
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?
} else {
req.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?
};
if res.status() == awc::http::StatusCode::NO_CONTENT {
return Ok(None);
} else if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.map_ok(Some)
.await
}
async fn put(
&self,
url: &str,
body: &serde_json::Value,
timeout: Duration,
username: &str,
) -> Result<serde_json::Value> {
let mut res = self
.put(url)
.header(
awc::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.basic_auth(username, None)
.timeout(timeout)
.send_json(&body)
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn delete(&self, url: &str, timeout: Duration, username: &str) -> Result<()> {
let mut res = self
.delete(url)
.header(
awc::http::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.basic_auth(username, None)
.timeout(timeout)
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
Ok(())
}
}
#[cfg(feature = "awc")]
pub type AwcClient = Client<awc::Client>;
#[cfg(feature = "reqwest")]
#[async_trait(?Send)]
impl HttpClient for reqwest::Client {
async fn get(
&self,
url: &str,
query_params: &QueryParams,
timeout: Duration,
username: Option<&str>,
) -> Result<serde_json::Value> {
let mut req = self
.get(url)
.header(
reqwest::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.query(query_params)
.timeout(timeout);
if let Some(username) = username {
req = req.basic_auth::<_, String>(username, None);
}
let res = req
.query(&query_params)
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn post(
&self,
url: &str,
query_params: Option<&QueryParams>,
body: Option<&serde_json::Value>,
timeout: Duration,
username: Option<&str>,
) -> Result<Option<serde_json::Value>> {
let mut req = self
.post(url)
.header(
reqwest::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.timeout(timeout);
if let Some(username) = username {
req = req.basic_auth::<_, String>(username, None);
}
if let Some(query_params) = query_params {
req = req.query(query_params);
}
if let Some(body) = body {
req = req.json(&body);
}
let res = req
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if res.status() == reqwest::StatusCode::NO_CONTENT {
return Ok(None);
} else if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.map_ok(Some)
.await
}
async fn put(
&self,
url: &str,
body: &serde_json::Value,
timeout: Duration,
username: &str,
) -> Result<serde_json::Value> {
let res = self
.put(url)
.header(
reqwest::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.basic_auth::<_, String>(username, None)
.timeout(timeout)
.json(&body)
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
res.json()
.map_err(|err| Error::Server(err.to_string()))
.await
}
async fn delete(&self, url: &str, timeout: Duration, username: &str) -> Result<()> {
let res = self
.delete(url)
.header(
reqwest::header::USER_AGENT,
format!("sift-rust/{}", env!("CARGO_PKG_VERSION")),
)
.basic_auth::<_, String>(username, None)
.timeout(timeout)
.send()
.map_err(|err| {
tracing::error!(?err, "request error");
Error::Server(err.to_string())
})
.await?;
if !res.status().is_success() {
let error: Error = res
.json()
.map_err(|err| Error::Server(err.to_string()))
.await?;
return Err(error);
}
Ok(())
}
}
#[cfg(feature = "reqwest")]
pub type ReqwestClient = Client<reqwest::Client>;