use std::borrow::Cow;
use std::fmt::Display;
use std::time::Duration;
use bytes::Bytes;
use serde::Serialize;
use url::Url;
use crate::error::Error;
use crate::http::{HeaderName, HeaderValue, Method};
use crate::observability::OperationInfo;
use crate::route::Route;
pub const DEFAULT_RETRY_ON: &[u16] = &[429, 500, 503];
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RetryPolicy {
pub attempts: u32,
pub base_delay: Duration,
pub retry_on: Cow<'static, [u16]>,
}
impl RetryPolicy {
pub fn none() -> RetryPolicy {
RetryPolicy {
attempts: 1,
base_delay: Duration::ZERO,
retry_on: Cow::Borrowed(&[]),
}
}
pub fn retries(&self, status: u16) -> bool {
self.attempts > 1 && self.retry_on.contains(&status)
}
}
#[derive(Clone)]
pub struct Operation {
pub(crate) id: Cow<'static, str>,
pub(crate) info: OperationInfo,
pub(crate) method: Method,
pub(crate) path: String,
pub(crate) url: Option<Url>,
pub(crate) query: Vec<(String, String)>,
pub(crate) headers: Vec<(HeaderName, HeaderValue)>,
pub(crate) body: Option<Body>,
pub(crate) idempotent: bool,
pub(crate) retry: Option<RetryPolicy>,
pub(crate) no_cache: bool,
}
#[derive(Clone)]
pub(crate) struct Body {
pub(crate) content_type: String,
pub(crate) bytes: Bytes,
}
impl std::fmt::Debug for Body {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Body")
.field("content_type", &self.content_type)
.field("len", &self.bytes.len())
.finish()
}
}
impl std::fmt::Debug for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Operation")
.field("id", &self.id)
.field("method", &self.method)
.field("path", &self.path)
.field("query", &self.query)
.field("body", &self.body)
.field("idempotent", &self.idempotent)
.field("retry", &self.retry)
.finish_non_exhaustive()
}
}
impl Operation {
pub(crate) fn for_route(
route: &'static Route,
account_id: Option<&str>,
params: &[&dyn Display],
) -> Result<Operation, Error> {
let path = route.try_fill(account_id, params)?;
let retry = route
.retry
.map_or_else(RetryPolicy::none, |retry| RetryPolicy {
attempts: retry.max,
base_delay: Duration::from_millis(retry.base_delay_ms),
retry_on: Cow::Borrowed(retry.retry_on),
});
Ok(Operation {
id: Cow::Borrowed(route.id),
info: OperationInfo {
service: Cow::Borrowed(route.service),
operation: Cow::Borrowed(route.id),
resource_type: Cow::Borrowed(route.resource_type),
is_mutation: !route.readonly,
resource_id: None,
},
method: route.method.clone(),
path,
url: None,
query: Vec::new(),
headers: Vec::new(),
body: None,
idempotent: route.idempotent,
retry: Some(retry),
no_cache: false,
})
}
pub(crate) fn raw(method: Method, path: String) -> Operation {
let idempotent = method != Method::POST;
let id = format!("{method} {path}");
let info = OperationInfo {
service: Cow::Borrowed("Raw"),
operation: Cow::Owned(method.to_string()),
resource_type: Cow::Borrowed("raw"),
is_mutation: method != Method::GET,
resource_id: None,
};
Operation {
id: Cow::Owned(id),
info,
method,
path,
url: None,
query: Vec::new(),
headers: Vec::new(),
body: None,
idempotent,
retry: None,
no_cache: false,
}
}
pub(crate) fn at(method: Method, url: Url) -> Operation {
let mut operation = Operation::raw(method, url.path().to_string());
operation.url = Some(url);
operation
}
pub fn id(&self) -> &str {
&self.id
}
pub fn method(&self) -> &Method {
&self.method
}
pub fn path(&self) -> &str {
&self.path
}
pub fn retry_policy(&self) -> Option<&RetryPolicy> {
self.retry.as_ref()
}
pub fn query(&mut self, name: &str, value: impl Display) -> &mut Operation {
self.query.push((name.to_string(), value.to_string()));
self
}
pub fn query_optional<T: Display>(&mut self, name: &str, value: Option<&T>) -> &mut Operation {
if let Some(value) = value {
self.query(name, value);
}
self
}
pub fn query_all<T: Display>(&mut self, name: &str, values: &[T]) -> &mut Operation {
for value in values {
self.query(name, value);
}
self
}
pub fn query_all_optional<T: Display>(
&mut self,
name: &str,
values: Option<&[T]>,
) -> &mut Operation {
if let Some(values) = values {
self.query_all(name, values);
}
self
}
pub fn header(&mut self, name: HeaderName, value: HeaderValue) -> &mut Operation {
self.headers.push((name, value));
self
}
pub fn json<T: Serialize + ?Sized>(&mut self, body: &T) -> Result<&mut Operation, Error> {
self.body_bytes("application/json", Bytes::from(serde_json::to_vec(body)?));
Ok(self)
}
pub fn body_bytes(&mut self, content_type: impl Into<String>, bytes: Bytes) -> &mut Operation {
self.body = Some(Body {
content_type: content_type.into(),
bytes,
});
self
}
pub fn info(&mut self, info: OperationInfo) -> &mut Operation {
self.info = info;
self
}
pub fn operation_name(&mut self, name: impl Into<Cow<'static, str>>) -> &mut Operation {
self.info.operation = name.into();
self
}
pub fn resource_type(&mut self, resource_type: impl Into<Cow<'static, str>>) -> &mut Operation {
self.info.resource_type = resource_type.into();
self
}
pub fn resource_id(&mut self, resource_id: impl Display) -> &mut Operation {
self.info.resource_id = Some(resource_id.to_string());
self
}
pub fn idempotent(&mut self, idempotent: bool) -> &mut Operation {
self.idempotent = idempotent;
self
}
pub fn no_retry(&mut self) -> &mut Operation {
self.retry = Some(RetryPolicy::none());
self
}
pub fn retry(&mut self, policy: RetryPolicy) -> &mut Operation {
self.retry = Some(policy);
self
}
pub fn no_cache(&mut self) -> &mut Operation {
self.no_cache = true;
self
}
}
#[cfg(test)]
mod raw_scope {
use super::*;
#[test]
fn raw_calls_share_one_scope_per_verb() {
let one = Operation::raw(Method::GET, "/999/cards/1".to_string());
let two = Operation::raw(Method::GET, "/999/cards/2".to_string());
assert_eq!(one.info.operation, two.info.operation);
assert_eq!(one.info.operation, "GET");
assert_eq!(one.id, "GET /999/cards/1");
}
}