1use std::borrow::Cow;
4use std::fmt::Display;
5use std::time::Duration;
6
7use bytes::Bytes;
8use serde::Serialize;
9use url::Url;
10
11use crate::error::Error;
12use crate::http::{HeaderName, HeaderValue, Method};
13use crate::observability::OperationInfo;
14use crate::route::Route;
15
16pub const DEFAULT_RETRY_ON: &[u16] = &[429, 500, 503];
19
20#[derive(Debug, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct RetryPolicy {
26 pub attempts: u32,
28 pub base_delay: Duration,
30 pub retry_on: Cow<'static, [u16]>,
32}
33
34impl RetryPolicy {
35 pub fn none() -> RetryPolicy {
37 RetryPolicy {
38 attempts: 1,
39 base_delay: Duration::ZERO,
40 retry_on: Cow::Borrowed(&[]),
41 }
42 }
43
44 pub fn retries(&self, status: u16) -> bool {
46 self.attempts > 1 && self.retry_on.contains(&status)
47 }
48}
49
50#[derive(Clone)]
53pub struct Operation {
54 pub(crate) id: Cow<'static, str>,
55 pub(crate) info: OperationInfo,
56 pub(crate) method: Method,
57 pub(crate) path: String,
58 pub(crate) url: Option<Url>,
59 pub(crate) query: Vec<(String, String)>,
60 pub(crate) headers: Vec<(HeaderName, HeaderValue)>,
61 pub(crate) body: Option<Body>,
62 pub(crate) idempotent: bool,
63 pub(crate) retry: Option<RetryPolicy>,
66 pub(crate) no_cache: bool,
67}
68
69#[derive(Clone)]
70pub(crate) struct Body {
71 pub(crate) content_type: String,
72 pub(crate) bytes: Bytes,
73}
74
75impl std::fmt::Debug for Body {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("Body")
80 .field("content_type", &self.content_type)
81 .field("len", &self.bytes.len())
82 .finish()
83 }
84}
85
86impl std::fmt::Debug for Operation {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("Operation")
89 .field("id", &self.id)
90 .field("method", &self.method)
91 .field("path", &self.path)
92 .field("query", &self.query)
93 .field("body", &self.body)
94 .field("idempotent", &self.idempotent)
95 .field("retry", &self.retry)
96 .finish_non_exhaustive()
97 }
98}
99
100impl Operation {
101 pub(crate) fn for_route(
102 route: &'static Route,
103 account_id: Option<&str>,
104 params: &[&dyn Display],
105 ) -> Result<Operation, Error> {
106 let path = route.try_fill(account_id, params)?;
107 let retry = route
108 .retry
109 .map_or_else(RetryPolicy::none, |retry| RetryPolicy {
110 attempts: retry.max,
111 base_delay: Duration::from_millis(retry.base_delay_ms),
112 retry_on: Cow::Borrowed(retry.retry_on),
113 });
114 Ok(Operation {
115 id: Cow::Borrowed(route.id),
116 info: OperationInfo {
117 service: Cow::Borrowed(route.service),
118 operation: Cow::Borrowed(route.id),
119 resource_type: Cow::Borrowed(route.resource_type),
120 is_mutation: !route.readonly,
121 resource_id: None,
122 },
123 method: route.method.clone(),
124 path,
125 url: None,
126 query: Vec::new(),
127 headers: Vec::new(),
128 body: None,
129 idempotent: route.idempotent,
130 retry: Some(retry),
131 no_cache: false,
132 })
133 }
134
135 pub(crate) fn raw(method: Method, path: String) -> Operation {
139 let idempotent = method != Method::POST;
140 let id = format!("{method} {path}");
141 let info = OperationInfo {
144 service: Cow::Borrowed("Raw"),
145 operation: Cow::Owned(method.to_string()),
146 resource_type: Cow::Borrowed("raw"),
147 is_mutation: method != Method::GET,
148 resource_id: None,
149 };
150 Operation {
151 id: Cow::Owned(id),
152 info,
153 method,
154 path,
155 url: None,
156 query: Vec::new(),
157 headers: Vec::new(),
158 body: None,
159 idempotent,
160 retry: None,
161 no_cache: false,
162 }
163 }
164
165 pub(crate) fn at(method: Method, url: Url) -> Operation {
166 let mut operation = Operation::raw(method, url.path().to_string());
167 operation.url = Some(url);
168 operation
169 }
170
171 pub fn id(&self) -> &str {
173 &self.id
174 }
175
176 pub fn method(&self) -> &Method {
178 &self.method
179 }
180
181 pub fn path(&self) -> &str {
183 &self.path
184 }
185
186 pub fn retry_policy(&self) -> Option<&RetryPolicy> {
188 self.retry.as_ref()
189 }
190
191 pub fn query(&mut self, name: &str, value: impl Display) -> &mut Operation {
193 self.query.push((name.to_string(), value.to_string()));
194 self
195 }
196
197 pub fn query_optional<T: Display>(&mut self, name: &str, value: Option<&T>) -> &mut Operation {
199 if let Some(value) = value {
200 self.query(name, value);
201 }
202 self
203 }
204
205 pub fn query_all<T: Display>(&mut self, name: &str, values: &[T]) -> &mut Operation {
207 for value in values {
208 self.query(name, value);
209 }
210 self
211 }
212
213 pub fn query_all_optional<T: Display>(
215 &mut self,
216 name: &str,
217 values: Option<&[T]>,
218 ) -> &mut Operation {
219 if let Some(values) = values {
220 self.query_all(name, values);
221 }
222 self
223 }
224
225 pub fn header(&mut self, name: HeaderName, value: HeaderValue) -> &mut Operation {
228 self.headers.push((name, value));
229 self
230 }
231
232 pub fn json<T: Serialize + ?Sized>(&mut self, body: &T) -> Result<&mut Operation, Error> {
234 self.body_bytes("application/json", Bytes::from(serde_json::to_vec(body)?));
235 Ok(self)
236 }
237
238 pub fn body_bytes(&mut self, content_type: impl Into<String>, bytes: Bytes) -> &mut Operation {
240 self.body = Some(Body {
241 content_type: content_type.into(),
242 bytes,
243 });
244 self
245 }
246
247 pub fn info(&mut self, info: OperationInfo) -> &mut Operation {
250 self.info = info;
251 self
252 }
253
254 pub fn operation_name(&mut self, name: impl Into<Cow<'static, str>>) -> &mut Operation {
256 self.info.operation = name.into();
257 self
258 }
259
260 pub fn resource_type(&mut self, resource_type: impl Into<Cow<'static, str>>) -> &mut Operation {
262 self.info.resource_type = resource_type.into();
263 self
264 }
265
266 pub fn resource_id(&mut self, resource_id: impl Display) -> &mut Operation {
269 self.info.resource_id = Some(resource_id.to_string());
270 self
271 }
272
273 pub fn idempotent(&mut self, idempotent: bool) -> &mut Operation {
276 self.idempotent = idempotent;
277 self
278 }
279
280 pub fn no_retry(&mut self) -> &mut Operation {
282 self.retry = Some(RetryPolicy::none());
283 self
284 }
285
286 pub fn retry(&mut self, policy: RetryPolicy) -> &mut Operation {
289 self.retry = Some(policy);
290 self
291 }
292
293 pub fn no_cache(&mut self) -> &mut Operation {
295 self.no_cache = true;
296 self
297 }
298}
299
300#[cfg(test)]
301mod raw_scope {
302 use super::*;
303
304 #[test]
305 fn raw_calls_share_one_scope_per_verb() {
306 let one = Operation::raw(Method::GET, "/999/cards/1".to_string());
307 let two = Operation::raw(Method::GET, "/999/cards/2".to_string());
308 assert_eq!(one.info.operation, two.info.operation);
309 assert_eq!(one.info.operation, "GET");
310 assert_eq!(one.id, "GET /999/cards/1");
311 }
312}