1use std::{any::type_name, borrow::Cow, collections::BTreeMap};
2
3use schemars::JsonSchema;
4use topcoat_router::StatusCode;
5
6use crate::{
7 Content, Parameter,
8 content::{ContentType, SchemaFn},
9};
10
11#[derive(Debug, Clone, Default)]
67pub struct Operation {
68 pub(crate) summary: Option<Cow<'static, str>>,
69 pub(crate) description: Option<Cow<'static, str>>,
70 pub(crate) id: Option<Cow<'static, str>>,
71 pub(crate) deprecated: Option<bool>,
72 pub(crate) hidden: Option<bool>,
73 pub(crate) tags: Vec<Cow<'static, str>>,
74 pub(crate) parameters: Vec<Parameter>,
75 pub(crate) queries: Vec<QueryType>,
76 pub(crate) request: Option<ContentType>,
77 pub(crate) responses: BTreeMap<u16, ResponseType>,
78 pub(crate) security: Option<Vec<Cow<'static, str>>>,
79}
80
81impl Operation {
82 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 #[must_use]
90 pub fn summary(mut self, summary: impl Into<Cow<'static, str>>) -> Self {
91 self.summary = Some(summary.into());
92 self
93 }
94
95 #[must_use]
97 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
98 self.description = Some(description.into());
99 self
100 }
101
102 #[must_use]
108 pub fn operation_id(mut self, operation_id: impl Into<Cow<'static, str>>) -> Self {
109 self.id = Some(operation_id.into());
110 self
111 }
112
113 #[must_use]
115 pub fn deprecated(mut self, deprecated: bool) -> Self {
116 self.deprecated = Some(deprecated);
117 self
118 }
119
120 #[must_use]
125 pub fn hidden(mut self, hidden: bool) -> Self {
126 self.hidden = Some(hidden);
127 self
128 }
129
130 #[must_use]
132 pub fn tag(mut self, tag: impl Into<Cow<'static, str>>) -> Self {
133 let tag = tag.into();
134 if !self.tags.contains(&tag) {
135 self.tags.push(tag);
136 }
137 self
138 }
139
140 #[must_use]
144 pub fn parameter(mut self, parameter: Parameter) -> Self {
145 self.parameters.retain(|other| !other.same_as(¶meter));
146 self.parameters.push(parameter);
147 self
148 }
149
150 #[must_use]
172 pub fn query<T: JsonSchema>(mut self) -> Self {
173 self.queries.push(QueryType {
174 name: type_name::<T>(),
175 schema: T::json_schema,
176 });
177 self
178 }
179
180 #[must_use]
185 pub fn request<T: Content>(mut self) -> Self {
186 self.request = Some(ContentType::of::<T>());
187 self
188 }
189
190 #[must_use]
195 pub fn response(
196 mut self,
197 status: StatusCode,
198 description: impl Into<Cow<'static, str>>,
199 ) -> Self {
200 self.responses.insert(
201 status.as_u16(),
202 ResponseType {
203 description: description.into(),
204 content: None,
205 },
206 );
207 self
208 }
209
210 #[must_use]
215 pub fn response_with<T: Content>(
216 mut self,
217 status: StatusCode,
218 description: impl Into<Cow<'static, str>>,
219 ) -> Self {
220 self.responses.insert(
221 status.as_u16(),
222 ResponseType {
223 description: description.into(),
224 content: Some(ContentType::of::<T>()),
225 },
226 );
227 self
228 }
229
230 #[must_use]
240 pub fn security(mut self, scheme: impl Into<Cow<'static, str>>) -> Self {
241 self.security.get_or_insert_default().push(scheme.into());
242 self
243 }
244
245 #[must_use]
248 pub fn no_security(mut self) -> Self {
249 self.security = Some(Vec::new());
250 self
251 }
252
253 pub(crate) fn combine<'a>(levels: impl IntoIterator<Item = &'a Operation>) -> Option<Self> {
257 let mut levels = levels.into_iter();
258 let mut combined = levels.next()?.clone();
259 for outer in levels {
260 combined.inherit(outer);
261 }
262 Some(combined)
263 }
264
265 fn inherit(&mut self, outer: &Operation) {
268 fn fill<T: Clone>(field: &mut Option<T>, outer: Option<&T>) {
269 if field.is_none() {
270 *field = outer.cloned();
271 }
272 }
273
274 fill(&mut self.summary, outer.summary.as_ref());
275 fill(&mut self.description, outer.description.as_ref());
276 fill(&mut self.id, outer.id.as_ref());
277 fill(&mut self.deprecated, outer.deprecated.as_ref());
278 fill(&mut self.hidden, outer.hidden.as_ref());
279 fill(&mut self.request, outer.request.as_ref());
280 fill(&mut self.security, outer.security.as_ref());
281
282 let mut tags = outer.tags.clone();
283 for tag in self.tags.drain(..) {
284 if !tags.contains(&tag) {
285 tags.push(tag);
286 }
287 }
288 self.tags = tags;
289
290 let mut parameters: Vec<Parameter> = outer
291 .parameters
292 .iter()
293 .filter(|parameter| !self.parameters.iter().any(|own| own.same_as(parameter)))
294 .cloned()
295 .collect();
296 parameters.append(&mut self.parameters);
297 self.parameters = parameters;
298
299 let mut queries = outer.queries.clone();
300 queries.append(&mut self.queries);
301 self.queries = queries;
302
303 for (status, response) in &outer.responses {
304 self.responses
305 .entry(*status)
306 .or_insert_with(|| response.clone());
307 }
308 }
309}
310
311#[derive(Debug, Clone, Copy)]
313pub(crate) struct QueryType {
314 pub(crate) name: &'static str,
316 pub(crate) schema: SchemaFn,
317}
318
319#[derive(Debug, Clone)]
321pub(crate) struct ResponseType {
322 pub(crate) description: Cow<'static, str>,
323 pub(crate) content: Option<ContentType>,
324}
325
326#[cfg(test)]
327mod tests {
328 use topcoat_router::content::Json;
329
330 use super::*;
331
332 fn combine(levels: &[Operation]) -> Operation {
334 Operation::combine(levels).unwrap()
335 }
336
337 #[test]
338 fn combining_nothing_yields_no_operation() {
339 assert!(Operation::combine(&[]).is_none());
340 }
341
342 #[test]
343 fn the_closest_value_of_a_setting_takes_priority() {
344 let layer = Operation::new()
345 .summary("Layer")
346 .description("From the layer");
347 let route = Operation::new().summary("Route");
348
349 let combined = combine(&[route, layer]);
350
351 assert_eq!(combined.summary.as_deref(), Some("Route"));
352 assert_eq!(combined.description.as_deref(), Some("From the layer"));
353 }
354
355 #[test]
356 fn a_route_can_show_itself_below_a_layer_that_hides_its_routes() {
357 let layer = Operation::new().hidden(true);
358
359 assert_eq!(
360 combine(&[Operation::new(), layer.clone()]).hidden,
361 Some(true)
362 );
363 assert_eq!(
364 combine(&[Operation::new().hidden(false), layer]).hidden,
365 Some(false)
366 );
367 }
368
369 #[test]
370 fn tags_add_up_from_the_outermost_level_without_repeating() {
371 let app = Operation::new().tag("api");
372 let layer = Operation::new().tag("users");
373 let route = Operation::new().tag("api").tag("admin");
374
375 let combined = combine(&[route, layer, app]);
376
377 assert_eq!(combined.tags, ["api", "users", "admin"]);
378 }
379
380 #[test]
381 fn responses_add_up_and_the_closest_one_wins_per_status() {
382 let layer = Operation::new()
383 .response(StatusCode::UNAUTHORIZED, "Not signed in.")
384 .response(StatusCode::NOT_FOUND, "Layer");
385 let route = Operation::new()
386 .response(StatusCode::NOT_FOUND, "Route")
387 .response_with::<Json<String>>(StatusCode::OK, "Found.");
388
389 let combined = combine(&[route, layer]);
390
391 let statuses: Vec<u16> = combined.responses.keys().copied().collect();
392 assert_eq!(statuses, [200, 401, 404]);
393 assert_eq!(combined.responses[&404].description, "Route");
394 assert!(combined.responses[&200].content.is_some());
395 assert!(combined.responses[&401].content.is_none());
396 }
397
398 #[test]
399 fn parameters_add_up_and_the_closest_one_wins_per_name_and_location() {
400 let layer = Operation::new()
401 .parameter(Parameter::header("x-tenant").description("Layer"))
402 .parameter(Parameter::query("page"));
403 let route = Operation::new()
404 .parameter(Parameter::header("x-tenant").description("Route"))
405 .parameter(Parameter::cookie("page"));
407
408 let combined = combine(&[route, layer]);
409
410 let described: Vec<_> = combined
411 .parameters
412 .iter()
413 .map(|parameter| {
414 (
415 parameter.location,
416 &*parameter.name,
417 parameter.description.as_deref(),
418 )
419 })
420 .collect();
421 assert_eq!(
422 described,
423 [
424 (crate::parameter::ParameterLocation::Query, "page", None),
425 (
426 crate::parameter::ParameterLocation::Header,
427 "x-tenant",
428 Some("Route")
429 ),
430 (crate::parameter::ParameterLocation::Cookie, "page", None),
431 ]
432 );
433 }
434
435 #[test]
436 fn describing_a_parameter_again_replaces_it() {
437 let operation = Operation::new()
438 .parameter(Parameter::query("page").description("First"))
439 .parameter(Parameter::query("page").description("Second"));
440
441 assert_eq!(operation.parameters.len(), 1);
442 assert_eq!(
443 operation.parameters[0].description.as_deref(),
444 Some("Second")
445 );
446 }
447
448 #[test]
449 fn security_requirements_replace_those_of_the_layers_as_a_whole() {
450 let layer = Operation::new().security("token").security("key");
451
452 let inherited = combine(&[Operation::new(), layer.clone()]);
453 assert_eq!(inherited.security.unwrap(), ["token", "key"]);
454
455 let replaced = combine(&[Operation::new().security("session"), layer.clone()]);
456 assert_eq!(replaced.security.unwrap(), ["session"]);
457
458 let public = combine(&[Operation::new().no_security(), layer]);
459 assert_eq!(public.security, Some(Vec::new()));
460 }
461}