1use std::borrow::Cow;
2use std::fmt::Display;
3
4use crate::http::Method;
5use bytes::Bytes;
6use serde::Serialize;
7use url::Url;
8
9use crate::error::Error;
10use crate::observability::OperationInfo;
11use crate::route::Route;
12
13#[derive(Clone)]
16#[allow(clippy::struct_excessive_bools)] pub struct Operation {
18 pub(crate) id: Cow<'static, str>,
19 pub(crate) info: OperationInfo,
20 pub(crate) route: Option<&'static Route>,
23 pub(crate) method: Method,
24 pub(crate) path: String,
25 pub(crate) url: Option<Url>,
26 pub(crate) query: Vec<(String, String)>,
27 pub(crate) body: Option<Body>,
28 pub(crate) idempotent: bool,
29 pub(crate) empty_on: &'static [u16],
30 pub(crate) accept: &'static str,
31 pub(crate) json_suffix: bool,
34 pub(crate) no_cache: bool,
35 pub(crate) capture_redirects: bool,
36 pub(crate) quiet: bool,
39}
40
41#[derive(Clone)]
42pub(crate) struct Body {
43 pub(crate) content_type: String,
44 pub(crate) bytes: Bytes,
45}
46
47impl std::fmt::Debug for Operation {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 let query: Vec<&str> = self.query.iter().map(|(name, _)| name.as_str()).collect();
54 let without_query =
55 |value: &str| -> String { value.split('?').next().unwrap_or_default().to_string() };
56 f.debug_struct("Operation")
57 .field("id", &without_query(&self.id))
58 .field("method", &self.method)
59 .field("path", &without_query(&self.path))
60 .field("query", &query)
61 .field("body", &self.body)
62 .field("idempotent", &self.idempotent)
63 .field("quiet", &self.quiet)
64 .finish_non_exhaustive()
65 }
66}
67
68impl std::fmt::Debug for Body {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("Body")
73 .field("content_type", &self.content_type)
74 .field("len", &self.bytes.len())
75 .finish()
76 }
77}
78
79const REDIRECTS: &[u16] = &[302, 303];
82
83impl Operation {
84 pub(crate) fn for_route(route: &'static Route, params: &[&dyn Display]) -> Operation {
85 Operation {
86 id: Cow::Borrowed(route.id),
87 route: Some(route),
88 info: OperationInfo {
89 service: Cow::Borrowed(route.service),
90 operation: Cow::Borrowed(route.id),
91 resource_type: Cow::Borrowed(route.resource_type),
92 is_mutation: !route.readonly,
93 resource_id: None,
94 },
95 method: route.method.clone(),
96 path: route.fill(params),
97 url: None,
98 query: Vec::new(),
99 body: None,
100 idempotent: route.idempotent,
101 empty_on: route.empty_on,
102 accept: if route.html {
103 "text/html"
104 } else {
105 "application/json"
106 },
107 json_suffix: !route.html,
108 no_cache: false,
109 capture_redirects: false,
110 quiet: false,
111 }
112 }
113
114 pub(crate) fn raw(method: Method, path: String) -> Operation {
115 let idempotent = matches!(
116 method,
117 Method::GET | Method::HEAD | Method::PUT | Method::DELETE
118 );
119 let id = format!("{method} {path}");
120 let info = OperationInfo {
121 service: Cow::Borrowed("Raw"),
122 operation: Cow::Owned(id.clone()),
123 resource_type: Cow::Borrowed("raw"),
124 is_mutation: method != Method::GET,
125 resource_id: None,
126 };
127 Operation {
128 id: Cow::Owned(id),
129 route: None,
130 info,
131 method,
132 path,
133 url: None,
134 query: Vec::new(),
135 body: None,
136 idempotent,
137 empty_on: &[],
138 accept: "application/json",
139 json_suffix: true,
140 no_cache: false,
141 capture_redirects: false,
142 quiet: false,
143 }
144 }
145
146 pub(crate) fn at(method: Method, url: Url) -> Operation {
147 let mut operation = Operation::raw(method, url.path().to_string());
148 operation.url = Some(url);
149 operation
150 }
151
152 pub fn id(&self) -> &str {
154 &self.id
155 }
156
157 pub(crate) fn label(&self) -> &str {
161 if self.info.service == "Raw" {
162 self.method.as_str()
163 } else {
164 &self.info.operation
165 }
166 }
167
168 pub fn method(&self) -> &Method {
170 &self.method
171 }
172
173 pub fn path(&self) -> &str {
175 &self.path
176 }
177
178 pub fn query(&mut self, name: &str, value: impl Display) -> &mut Operation {
180 self.query.push((name.to_string(), value.to_string()));
181 self
182 }
183
184 pub fn query_optional<T: Display>(&mut self, name: &str, value: Option<&T>) -> &mut Operation {
186 if let Some(value) = value {
187 self.query(name, value);
188 }
189 self
190 }
191
192 pub fn json<T: Serialize + ?Sized>(&mut self, body: &T) -> Result<&mut Operation, Error> {
194 self.body_bytes("application/json", Bytes::from(serde_json::to_vec(body)?));
195 Ok(self)
196 }
197
198 pub fn form(&mut self, fields: &[(&str, &str)]) -> &mut Operation {
200 let encoded = url::form_urlencoded::Serializer::new(String::new())
201 .extend_pairs(fields)
202 .finish();
203 self.body_bytes("application/x-www-form-urlencoded", Bytes::from(encoded))
204 }
205
206 pub fn multipart(&mut self, content_type: String, body: Bytes) -> &mut Operation {
209 self.body_bytes(content_type, body)
210 }
211
212 pub fn body_bytes(&mut self, content_type: impl Into<String>, bytes: Bytes) -> &mut Operation {
214 self.body = Some(Body {
215 content_type: content_type.into(),
216 bytes,
217 });
218 self
219 }
220
221 pub fn info(&mut self, info: OperationInfo) -> &mut Operation {
224 self.info = info;
225 self
226 }
227
228 pub fn operation_name(&mut self, name: impl Into<Cow<'static, str>>) -> &mut Operation {
231 self.info.operation = name.into();
232 self
233 }
234
235 pub fn resource_type(&mut self, resource_type: impl Into<Cow<'static, str>>) -> &mut Operation {
237 self.info.resource_type = resource_type.into();
238 self
239 }
240
241 pub fn resource_id(&mut self, resource_id: i64) -> &mut Operation {
244 self.info.resource_id = Some(resource_id);
245 self
246 }
247
248 pub fn idempotent(&mut self, idempotent: bool) -> &mut Operation {
250 self.idempotent = idempotent;
251 self
252 }
253
254 pub fn accept(&mut self, media_type: &'static str) -> &mut Operation {
256 self.accept = media_type;
257 self
258 }
259
260 pub fn without_json_suffix(&mut self) -> &mut Operation {
263 self.json_suffix = false;
264 self
265 }
266
267 pub fn no_cache(&mut self) -> &mut Operation {
270 self.no_cache = true;
271 self
272 }
273
274 pub fn capture_redirects(&mut self) -> &mut Operation {
278 self.capture_redirects = true;
279 self.empty_on = REDIRECTS;
280 self
281 }
282
283 pub fn form_representation(&mut self) -> &mut Operation {
286 self.without_json_suffix().accept("*/*")
287 }
288
289 pub fn quiet(&mut self) -> &mut Operation {
297 self.quiet = true;
298 self
299 }
300}