Skip to main content

a3s_boot/routing/route/
openapi.rs

1use super::definition::RouteDefinition;
2#[cfg(feature = "openapi-schemas")]
3use crate::{openapi_schema_name, BootError};
4use crate::{
5    OpenApiApiKeyLocation, OpenApiExample, OpenApiHeader, OpenApiOAuthFlows, OpenApiParameter,
6    OpenApiParameterLocation, OpenApiRef, OpenApiReferenceOr, OpenApiRequestBody, OpenApiResponse,
7    OpenApiRouteMetadata, OpenApiSchema, OpenApiSecurityRequirement, OpenApiSecurityScheme,
8    OpenApiServer, Result,
9};
10use serde::Serialize;
11use serde_json::Value;
12
13impl RouteDefinition {
14    pub fn with_openapi(mut self, metadata: OpenApiRouteMetadata) -> Self {
15        self.openapi = metadata;
16        self
17    }
18
19    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
20        let tag = tag.into();
21        if !self.openapi.tags.contains(&tag) {
22            self.openapi.tags.push(tag);
23        }
24        self
25    }
26
27    pub fn with_operation_id(mut self, operation_id: impl Into<String>) -> Self {
28        self.openapi.operation_id = Some(operation_id.into());
29        self
30    }
31
32    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
33        self.openapi.summary = Some(summary.into());
34        self
35    }
36
37    pub fn with_description(mut self, description: impl Into<String>) -> Self {
38        self.openapi.description = Some(description.into());
39        self
40    }
41
42    pub fn with_deprecated(mut self) -> Self {
43        self.openapi.deprecated = true;
44        self
45    }
46
47    pub fn with_openapi_server(mut self, url: impl Into<String>) -> Self {
48        self.openapi.servers.push(OpenApiServer::new(url));
49        self
50    }
51
52    pub fn with_openapi_server_description(
53        mut self,
54        url: impl Into<String>,
55        description: impl Into<String>,
56    ) -> Self {
57        self.openapi
58            .servers
59            .push(OpenApiServer::new(url).with_description(description));
60        self
61    }
62
63    pub fn with_openapi_external_docs(
64        mut self,
65        description: impl Into<String>,
66        url: impl Into<String>,
67    ) -> Self {
68        self.openapi.external_docs = Some(crate::OpenApiExternalDocs::new(description, url));
69        self
70    }
71
72    pub fn with_openapi_extension_value(mut self, name: impl Into<String>, value: Value) -> Self {
73        self.openapi.extensions.insert(name.into(), value);
74        self
75    }
76
77    pub fn with_openapi_extension_default_value(
78        mut self,
79        name: impl Into<String>,
80        value: Value,
81    ) -> Self {
82        self.openapi.extensions.entry(name.into()).or_insert(value);
83        self
84    }
85
86    pub fn try_with_openapi_extension<T>(self, name: impl Into<String>, value: T) -> Result<Self>
87    where
88        T: Serialize,
89    {
90        let name = name.into();
91        let value = serde_json::to_value(value).map_err(|error| {
92            crate::BootError::Internal(format!(
93                "OpenAPI extension `{name}` could not be serialized: {error}"
94            ))
95        })?;
96        Ok(self.with_openapi_extension_value(name, value))
97    }
98
99    pub fn hide_from_openapi(mut self) -> Self {
100        self.openapi.hidden = true;
101        self
102    }
103
104    pub fn with_parameter(mut self, parameter: OpenApiParameter) -> Self {
105        upsert_parameter(
106            &mut self.openapi.parameters,
107            OpenApiReferenceOr::value(parameter),
108        );
109        self
110    }
111
112    pub fn with_parameter_ref(
113        mut self,
114        location: OpenApiParameterLocation,
115        name: impl Into<String>,
116        component_name: impl AsRef<str>,
117    ) -> Self {
118        let name = name.into();
119        let reference =
120            OpenApiRef::parameter(component_name).with_parameter_metadata(location, name);
121        upsert_parameter(
122            &mut self.openapi.parameters,
123            OpenApiReferenceOr::reference(reference),
124        );
125        self
126    }
127
128    pub fn with_path_parameter(self, name: impl Into<String>, schema: OpenApiSchema) -> Self {
129        self.with_parameter(OpenApiParameter::path(name, schema))
130    }
131
132    pub fn with_path_parameter_ref(
133        self,
134        name: impl Into<String>,
135        component_name: impl AsRef<str>,
136    ) -> Self {
137        self.with_parameter_ref(OpenApiParameterLocation::Path, name, component_name)
138    }
139
140    pub fn with_query_parameter(
141        self,
142        name: impl Into<String>,
143        required: bool,
144        schema: OpenApiSchema,
145    ) -> Self {
146        self.with_parameter(OpenApiParameter::query(name, required, schema))
147    }
148
149    pub fn with_query_parameter_ref(
150        self,
151        name: impl Into<String>,
152        component_name: impl AsRef<str>,
153    ) -> Self {
154        self.with_parameter_ref(OpenApiParameterLocation::Query, name, component_name)
155    }
156
157    pub fn with_header_parameter(
158        self,
159        name: impl Into<String>,
160        required: bool,
161        schema: OpenApiSchema,
162    ) -> Self {
163        self.with_parameter(OpenApiParameter::header(name, required, schema))
164    }
165
166    pub fn with_header_parameter_ref(
167        self,
168        name: impl Into<String>,
169        component_name: impl AsRef<str>,
170    ) -> Self {
171        self.with_parameter_ref(OpenApiParameterLocation::Header, name, component_name)
172    }
173
174    pub fn with_cookie_parameter(
175        self,
176        name: impl Into<String>,
177        required: bool,
178        schema: OpenApiSchema,
179    ) -> Self {
180        self.with_parameter(OpenApiParameter::cookie(name, required, schema))
181    }
182
183    pub fn with_cookie_parameter_ref(
184        self,
185        name: impl Into<String>,
186        component_name: impl AsRef<str>,
187    ) -> Self {
188        self.with_parameter_ref(OpenApiParameterLocation::Cookie, name, component_name)
189    }
190
191    pub fn with_request_body(mut self, request_body: OpenApiRequestBody) -> Self {
192        self.openapi.request_body = Some(OpenApiReferenceOr::value(request_body));
193        self
194    }
195
196    pub fn with_request_body_ref(mut self, component_name: impl AsRef<str>) -> Self {
197        self.openapi.request_body = Some(OpenApiReferenceOr::reference(OpenApiRef::request_body(
198            component_name,
199        )));
200        self
201    }
202
203    pub fn with_request_body_content_type(
204        self,
205        content_type: impl Into<String>,
206        schema: OpenApiSchema,
207    ) -> Self {
208        self.with_request_body(OpenApiRequestBody::content(content_type, schema))
209    }
210
211    pub fn with_json_request_body(self, schema: OpenApiSchema) -> Self {
212        self.with_request_body(OpenApiRequestBody::json(schema))
213    }
214
215    pub fn try_with_request_body_content_type_example<T>(
216        self,
217        content_type: impl Into<String>,
218        schema: OpenApiSchema,
219        example: T,
220    ) -> Result<Self>
221    where
222        T: Serialize,
223    {
224        Ok(
225            self.with_request_body(OpenApiRequestBody::try_content_example(
226                content_type,
227                schema,
228                example,
229            )?),
230        )
231    }
232
233    pub fn try_with_json_request_body_example<T>(
234        self,
235        schema: OpenApiSchema,
236        example: T,
237    ) -> Result<Self>
238    where
239        T: Serialize,
240    {
241        Ok(self.with_request_body(OpenApiRequestBody::try_json_example(schema, example)?))
242    }
243
244    pub fn try_with_request_body_content_type_named_example<T>(
245        self,
246        content_type: impl Into<String>,
247        schema: OpenApiSchema,
248        name: impl Into<String>,
249        example: T,
250    ) -> Result<Self>
251    where
252        T: Serialize,
253    {
254        let content_type = content_type.into();
255        let request_body = OpenApiRequestBody::content(content_type.clone(), schema)
256            .try_with_content_named_example(content_type, name, example)?;
257        Ok(self.with_request_body(request_body))
258    }
259
260    pub fn try_with_json_request_body_named_example<T>(
261        self,
262        schema: OpenApiSchema,
263        name: impl Into<String>,
264        example: T,
265    ) -> Result<Self>
266    where
267        T: Serialize,
268    {
269        Ok(self.with_request_body(
270            OpenApiRequestBody::json(schema).try_with_json_named_example(name, example)?,
271        ))
272    }
273
274    pub fn with_request_body_content_type_named_example_ref(
275        self,
276        content_type: impl Into<String>,
277        schema: OpenApiSchema,
278        name: impl Into<String>,
279        component_name: impl AsRef<str>,
280    ) -> Self {
281        let content_type = content_type.into();
282        let request_body = OpenApiRequestBody::content(content_type.clone(), schema)
283            .with_content_named_example_ref(content_type, name, component_name);
284        self.with_request_body(request_body)
285    }
286
287    pub fn with_json_request_body_named_example_ref(
288        self,
289        schema: OpenApiSchema,
290        name: impl Into<String>,
291        component_name: impl AsRef<str>,
292    ) -> Self {
293        self.with_request_body(
294            OpenApiRequestBody::json(schema).with_json_named_example_ref(name, component_name),
295        )
296    }
297
298    pub fn with_response(mut self, status: u16, response: OpenApiResponse) -> Self {
299        self.openapi
300            .responses
301            .insert(status.to_string(), OpenApiReferenceOr::value(response));
302        self
303    }
304
305    pub fn with_response_ref(mut self, status: u16, component_name: impl AsRef<str>) -> Self {
306        self.openapi.responses.insert(
307            status.to_string(),
308            OpenApiReferenceOr::reference(OpenApiRef::response(component_name)),
309        );
310        self
311    }
312
313    pub fn with_default_response(mut self, response: OpenApiResponse) -> Self {
314        self.openapi
315            .responses
316            .insert("default".to_string(), OpenApiReferenceOr::value(response));
317        self
318    }
319
320    pub fn with_default_response_ref(mut self, component_name: impl AsRef<str>) -> Self {
321        self.openapi.responses.insert(
322            "default".to_string(),
323            OpenApiReferenceOr::reference(OpenApiRef::response(component_name)),
324        );
325        self
326    }
327
328    pub fn with_openapi_response_header(
329        mut self,
330        status: u16,
331        name: impl Into<String>,
332        header: OpenApiHeader,
333    ) -> Self {
334        if let Some(response) = self
335            .openapi
336            .responses
337            .entry(status.to_string())
338            .or_insert_with(|| OpenApiReferenceOr::value(OpenApiResponse::description("Success")))
339            .value_mut()
340        {
341            response
342                .headers
343                .insert(name.into(), OpenApiReferenceOr::value(header));
344        }
345        self
346    }
347
348    pub fn with_openapi_response_header_ref(
349        mut self,
350        status: u16,
351        name: impl Into<String>,
352        component_name: impl AsRef<str>,
353    ) -> Self {
354        if let Some(response) = self
355            .openapi
356            .responses
357            .entry(status.to_string())
358            .or_insert_with(|| OpenApiReferenceOr::value(OpenApiResponse::description("Success")))
359            .value_mut()
360        {
361            response.headers.insert(
362                name.into(),
363                OpenApiReferenceOr::reference(OpenApiRef::header(component_name)),
364            );
365        }
366        self
367    }
368
369    pub fn with_default_openapi_response_header(
370        mut self,
371        name: impl Into<String>,
372        header: OpenApiHeader,
373    ) -> Self {
374        if let Some(response) = self
375            .openapi
376            .responses
377            .entry("default".to_string())
378            .or_insert_with(|| {
379                OpenApiReferenceOr::value(OpenApiResponse::description("Default response"))
380            })
381            .value_mut()
382        {
383            response
384                .headers
385                .insert(name.into(), OpenApiReferenceOr::value(header));
386        }
387        self
388    }
389
390    pub fn with_default_openapi_response_header_ref(
391        mut self,
392        name: impl Into<String>,
393        component_name: impl AsRef<str>,
394    ) -> Self {
395        if let Some(response) = self
396            .openapi
397            .responses
398            .entry("default".to_string())
399            .or_insert_with(|| {
400                OpenApiReferenceOr::value(OpenApiResponse::description("Default response"))
401            })
402            .value_mut()
403        {
404            response.headers.insert(
405                name.into(),
406                OpenApiReferenceOr::reference(OpenApiRef::header(component_name)),
407            );
408        }
409        self
410    }
411
412    pub fn with_response_content_type(
413        self,
414        status: u16,
415        description: impl Into<String>,
416        content_type: impl Into<String>,
417        schema: OpenApiSchema,
418    ) -> Self {
419        self.with_response(
420            status,
421            OpenApiResponse::content(description, content_type, schema),
422        )
423    }
424
425    pub fn with_json_response(
426        self,
427        status: u16,
428        description: impl Into<String>,
429        schema: OpenApiSchema,
430    ) -> Self {
431        self.with_response(status, OpenApiResponse::json(description, schema))
432    }
433
434    pub fn try_with_response_content_type_example<T>(
435        self,
436        status: u16,
437        description: impl Into<String>,
438        content_type: impl Into<String>,
439        schema: OpenApiSchema,
440        example: T,
441    ) -> Result<Self>
442    where
443        T: Serialize,
444    {
445        Ok(self.with_response(
446            status,
447            OpenApiResponse::try_content_example(description, content_type, schema, example)?,
448        ))
449    }
450
451    pub fn try_with_json_response_example<T>(
452        self,
453        status: u16,
454        description: impl Into<String>,
455        schema: OpenApiSchema,
456        example: T,
457    ) -> Result<Self>
458    where
459        T: Serialize,
460    {
461        Ok(self.with_response(
462            status,
463            OpenApiResponse::try_json_example(description, schema, example)?,
464        ))
465    }
466
467    pub fn try_with_response_content_type_named_example<T>(
468        self,
469        status: u16,
470        description: impl Into<String>,
471        content_type: impl Into<String>,
472        schema: OpenApiSchema,
473        name: impl Into<String>,
474        example: T,
475    ) -> Result<Self>
476    where
477        T: Serialize,
478    {
479        let content_type = content_type.into();
480        let response = OpenApiResponse::content(description, content_type.clone(), schema)
481            .try_with_content_named_example(content_type, name, example)?;
482        Ok(self.with_response(status, response))
483    }
484
485    pub fn try_with_json_response_named_example<T>(
486        self,
487        status: u16,
488        description: impl Into<String>,
489        schema: OpenApiSchema,
490        name: impl Into<String>,
491        example: T,
492    ) -> Result<Self>
493    where
494        T: Serialize,
495    {
496        Ok(self.with_response(
497            status,
498            OpenApiResponse::json(description, schema)
499                .try_with_json_named_example(name, example)?,
500        ))
501    }
502
503    pub fn with_response_content_type_named_example_ref(
504        self,
505        status: u16,
506        description: impl Into<String>,
507        content_type: impl Into<String>,
508        schema: OpenApiSchema,
509        name: impl Into<String>,
510        component_name: impl AsRef<str>,
511    ) -> Self {
512        let content_type = content_type.into();
513        let response = OpenApiResponse::content(description, content_type.clone(), schema)
514            .with_content_named_example_ref(content_type, name, component_name);
515        self.with_response(status, response)
516    }
517
518    pub fn with_json_response_named_example_ref(
519        self,
520        status: u16,
521        description: impl Into<String>,
522        schema: OpenApiSchema,
523        name: impl Into<String>,
524        component_name: impl AsRef<str>,
525    ) -> Self {
526        self.with_response(
527            status,
528            OpenApiResponse::json(description, schema)
529                .with_json_named_example_ref(name, component_name),
530        )
531    }
532
533    pub fn with_security_requirement(mut self, requirement: OpenApiSecurityRequirement) -> Self {
534        self.openapi.security.push(requirement);
535        self
536    }
537
538    pub fn with_api_security<I, S>(self, name: impl Into<String>, scopes: I) -> Self
539    where
540        I: IntoIterator<Item = S>,
541        S: Into<String>,
542    {
543        let mut requirement = OpenApiSecurityRequirement::new();
544        requirement.insert(
545            name.into(),
546            scopes.into_iter().map(Into::into).collect::<Vec<_>>(),
547        );
548        self.with_security_requirement(requirement)
549    }
550
551    pub fn with_security_scheme(
552        mut self,
553        name: impl Into<String>,
554        scheme: OpenApiSecurityScheme,
555    ) -> Self {
556        self.openapi.security_schemes.insert(name.into(), scheme);
557        self
558    }
559
560    pub fn with_bearer_auth(self) -> Self {
561        self.with_bearer_auth_named("bearerAuth")
562    }
563
564    pub fn with_bearer_auth_named(self, name: impl Into<String>) -> Self {
565        let name = name.into();
566        self.with_security_scheme(name.clone(), OpenApiSecurityScheme::http_bearer())
567            .with_api_security(name, Vec::<String>::new())
568    }
569
570    pub fn with_api_key_auth(
571        self,
572        scheme_name: impl Into<String>,
573        location: OpenApiApiKeyLocation,
574        key_name: impl Into<String>,
575    ) -> Self {
576        let scheme_name = scheme_name.into();
577        self.with_security_scheme(
578            scheme_name.clone(),
579            OpenApiSecurityScheme::api_key(location, key_name),
580        )
581        .with_api_security(scheme_name, Vec::<String>::new())
582    }
583
584    pub fn with_header_api_key_auth(
585        self,
586        scheme_name: impl Into<String>,
587        header_name: impl Into<String>,
588    ) -> Self {
589        self.with_api_key_auth(scheme_name, OpenApiApiKeyLocation::Header, header_name)
590    }
591
592    pub fn with_query_api_key_auth(
593        self,
594        scheme_name: impl Into<String>,
595        query_name: impl Into<String>,
596    ) -> Self {
597        self.with_api_key_auth(scheme_name, OpenApiApiKeyLocation::Query, query_name)
598    }
599
600    pub fn with_cookie_auth(
601        self,
602        scheme_name: impl Into<String>,
603        cookie_name: impl Into<String>,
604    ) -> Self {
605        self.with_api_key_auth(scheme_name, OpenApiApiKeyLocation::Cookie, cookie_name)
606    }
607
608    pub fn with_oauth2_auth<I, S>(
609        self,
610        scheme_name: impl Into<String>,
611        flows: OpenApiOAuthFlows,
612        scopes: I,
613    ) -> Self
614    where
615        I: IntoIterator<Item = S>,
616        S: Into<String>,
617    {
618        let scheme_name = scheme_name.into();
619        self.with_security_scheme(scheme_name.clone(), OpenApiSecurityScheme::oauth2(flows))
620            .with_api_security(scheme_name, scopes)
621    }
622
623    pub fn with_open_id_connect_auth<I, S>(
624        self,
625        scheme_name: impl Into<String>,
626        url: impl Into<String>,
627        scopes: I,
628    ) -> Self
629    where
630        I: IntoIterator<Item = S>,
631        S: Into<String>,
632    {
633        let scheme_name = scheme_name.into();
634        self.with_security_scheme(
635            scheme_name.clone(),
636            OpenApiSecurityScheme::open_id_connect(url),
637        )
638        .with_api_security(scheme_name, scopes)
639    }
640
641    pub fn with_schema_component(mut self, name: impl Into<String>, schema: OpenApiSchema) -> Self {
642        self.openapi.schema_components.insert(name.into(), schema);
643        self
644    }
645
646    pub fn with_response_component(
647        mut self,
648        name: impl Into<String>,
649        response: OpenApiResponse,
650    ) -> Self {
651        self.openapi
652            .response_components
653            .insert(name.into(), response);
654        self
655    }
656
657    pub fn with_parameter_component(
658        mut self,
659        name: impl Into<String>,
660        parameter: OpenApiParameter,
661    ) -> Self {
662        self.openapi
663            .parameter_components
664            .insert(name.into(), parameter);
665        self
666    }
667
668    pub fn with_example_component(
669        mut self,
670        name: impl Into<String>,
671        example: OpenApiExample,
672    ) -> Self {
673        self.openapi.example_components.insert(name.into(), example);
674        self
675    }
676
677    pub fn try_with_example_component<T>(self, name: impl Into<String>, value: T) -> Result<Self>
678    where
679        T: Serialize,
680    {
681        Ok(self.with_example_component(name, OpenApiExample::try_value(value)?))
682    }
683
684    pub fn with_request_body_component(
685        mut self,
686        name: impl Into<String>,
687        request_body: OpenApiRequestBody,
688    ) -> Self {
689        self.openapi
690            .request_body_components
691            .insert(name.into(), request_body);
692        self
693    }
694
695    pub fn with_header_component(mut self, name: impl Into<String>, header: OpenApiHeader) -> Self {
696        self.openapi.header_components.insert(name.into(), header);
697        self
698    }
699
700    #[cfg(feature = "openapi-schemas")]
701    pub fn try_with_json_schema_component<T>(self) -> Result<Self>
702    where
703        T: schemars::JsonSchema,
704    {
705        let schema = OpenApiSchema::json_schema::<T>()
706            .map_err(|error| BootError::Internal(error.to_string()))?;
707        Ok(self.with_schema_component(openapi_schema_name::<T>(), schema))
708    }
709}
710
711fn upsert_parameter(
712    parameters: &mut Vec<OpenApiReferenceOr<OpenApiParameter>>,
713    parameter: OpenApiReferenceOr<OpenApiParameter>,
714) {
715    if let Some((location, name)) = parameter.parameter_identity() {
716        if let Some(existing) = parameters.iter_mut().find(|existing| {
717            existing
718                .parameter_identity()
719                .is_some_and(|(existing_location, existing_name)| {
720                    existing_location == location && existing_name == name
721                })
722        }) {
723            *existing = parameter;
724            return;
725        }
726    }
727
728    parameters.push(parameter);
729}