1use crate::ApiErrorComponent;
2#[cfg(feature = "actix")]
3use crate::{PathItemDefinition, ResponseWrapper};
4#[cfg(feature = "actix")]
5use actix_web::Either;
6use apistos_models::Schema;
7use apistos_models::paths::{MediaType, Parameter, RequestBody, Response, Responses};
8use apistos_models::reference_or::ReferenceOr;
9use apistos_models::security::SecurityScheme;
10#[cfg(feature = "actix")]
11use schemars::schema::SubschemaValidation;
12use schemars::schema::{ArrayValidation, InstanceType, SchemaObject, SingleOrVec};
13use std::collections::BTreeMap;
14#[cfg(feature = "actix")]
15use std::future::Future;
16
17pub trait ApiComponent {
18  fn content_type() -> String {
19    "application/json".to_string()
20  }
21
22  fn required() -> bool {
23    true
24  }
25
26  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)>;
29
30  fn raw_schema() -> Option<ReferenceOr<Schema>> {
31    None
32  }
33
34  fn schema() -> Option<(String, ReferenceOr<Schema>)>;
35
36  fn securities() -> BTreeMap<String, SecurityScheme> {
37    Default::default()
38  }
39
40  fn security_requirement_name() -> Option<String> {
41    None
42  }
43
44  fn request_body() -> Option<RequestBody> {
45    Self::schema().map(|(name, _)| RequestBody {
46      content: BTreeMap::from_iter(vec![(
47        Self::content_type(),
48        MediaType {
49          schema: Some(ReferenceOr::Reference {
50            _ref: format!("#/components/schemas/{}", name),
51          }),
52          ..Default::default()
53        },
54      )]),
55      required: Some(Self::required()),
56      ..Default::default()
57    })
58  }
59
60  fn error_responses() -> Vec<(String, Response)> {
61    vec![]
62  }
63
64  fn error_schemas() -> BTreeMap<String, (String, ReferenceOr<Schema>)> {
65    BTreeMap::default()
66  }
67
68  fn responses(_content_type: Option<String>) -> Option<Responses> {
69    None
70  }
71
72  fn parameters() -> Vec<Parameter> {
73    vec![]
74  }
75}
76
77impl<T> ApiComponent for Option<T>
78where
79  T: ApiComponent,
80{
81  fn required() -> bool {
82    false
83  }
84
85  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
86    T::child_schemas()
87  }
88
89  fn raw_schema() -> Option<ReferenceOr<Schema>> {
90    T::raw_schema()
91  }
92
93  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
94    T::schema()
95  }
96
97  fn securities() -> BTreeMap<String, SecurityScheme> {
98    T::securities()
99  }
100
101  fn security_requirement_name() -> Option<String> {
102    T::security_requirement_name()
103  }
104}
105
106impl<T> ApiComponent for Vec<T>
107where
108  T: ApiComponent,
109{
110  fn required() -> bool {
111    true
112  }
113
114  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
115    let mut schemas = T::schema().into_iter().collect::<Vec<(String, ReferenceOr<Schema>)>>();
116    schemas.append(&mut T::child_schemas());
117    schemas
118  }
119
120  fn raw_schema() -> Option<ReferenceOr<Schema>> {
121    T::raw_schema()
122  }
123
124  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
125    T::schema().map(|(name, schema)| {
126      let _ref = match schema {
127        ReferenceOr::Reference { _ref } => _ref,
128        ReferenceOr::Object(_) => format!("#/components/schemas/{}", name),
129      };
130
131      (
132        name,
133        ReferenceOr::Object(Schema::Object(SchemaObject {
134          instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
135          array: Some(Box::new(ArrayValidation {
136            items: Some(Schema::new_ref(_ref).into()),
137            ..Default::default()
138          })),
139          ..Default::default()
140        })),
141      )
142    })
143  }
144}
145
146impl<T, E> ApiComponent for Result<T, E>
147where
148  T: ApiComponent,
149  E: ApiErrorComponent,
150{
151  fn required() -> bool {
152    T::required()
153  }
154
155  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
156    T::child_schemas()
157  }
158
159  fn raw_schema() -> Option<ReferenceOr<Schema>> {
160    T::raw_schema()
161  }
162
163  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
164    T::schema()
165  }
166
167  fn error_responses() -> Vec<(String, Response)> {
169    E::error_responses()
170  }
171
172  fn error_schemas() -> BTreeMap<String, (String, ReferenceOr<Schema>)> {
174    E::schemas_by_status_code()
175  }
176
177  fn responses(content_type: Option<String>) -> Option<Responses> {
178    T::responses(content_type)
179  }
180}
181
182#[cfg(feature = "actix")]
183impl<T, E> ApiComponent for Either<T, E>
184where
185  T: ApiComponent,
186  E: ApiComponent,
187{
188  fn required() -> bool {
189    T::required() && E::required()
190  }
191
192  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
193    let mut child_schemas = T::child_schemas();
194    child_schemas.append(&mut E::child_schemas());
195    child_schemas
196  }
197
198  fn raw_schema() -> Option<ReferenceOr<Schema>> {
199    match (T::raw_schema(), E::raw_schema()) {
200      (Some(raw_schema1), Some(raw_schema2)) => {
201        let raw_schema1 = match raw_schema1 {
202          ReferenceOr::Object(schema_obj) => schema_obj,
203          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
204            reference: Some(_ref),
205            ..Default::default()
206          }),
207        };
208        let raw_schema2 = match raw_schema2 {
209          ReferenceOr::Object(schema_obj) => schema_obj,
210          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
211            reference: Some(_ref),
212            ..Default::default()
213          }),
214        };
215        Some(ReferenceOr::Object(Schema::Object(SchemaObject {
216          subschemas: Some(Box::new(SubschemaValidation {
217            one_of: Some(vec![raw_schema1, raw_schema2]),
218            ..Default::default()
219          })),
220          ..Default::default()
221        })))
222      }
223      (Some(raw_schema1), None) => Some(raw_schema1),
224      (None, Some(raw_schema2)) => Some(raw_schema2),
225      (None, None) => None,
226    }
227  }
228
229  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
230    match (T::schema(), E::schema()) {
231      (Some(schema1), Some(schema2)) => {
232        let (schema_name1, schema1) = schema1;
233        let schema1 = match schema1 {
234          ReferenceOr::Object(schema_obj) => schema_obj,
235          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
236            reference: Some(_ref),
237            ..Default::default()
238          }),
239        };
240        let (schema_name2, schema2) = schema2;
241        let schema2 = match schema2 {
242          ReferenceOr::Object(schema_obj) => schema_obj,
243          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
244            reference: Some(_ref),
245            ..Default::default()
246          }),
247        };
248        let schema = ReferenceOr::Object(Schema::Object(SchemaObject {
249          subschemas: Some(Box::new(SubschemaValidation {
250            one_of: Some(vec![schema1, schema2]),
251            ..Default::default()
252          })),
253          ..Default::default()
254        }));
255        let schema_name = format!("Either{}Or{}", schema_name1, schema_name2);
256        Some((schema_name, schema))
257      }
258      (Some(schema1), None) => Some(schema1),
259      (None, Some(schema2)) => Some(schema2),
260      (None, None) => None,
261    }
262  }
263
264  fn error_responses() -> Vec<(String, Response)> {
265    let mut error_responses = T::error_responses();
266    error_responses.append(&mut E::error_responses());
267    error_responses
268  }
269
270  fn error_schemas() -> BTreeMap<String, (String, ReferenceOr<Schema>)> {
271    let mut error_schemas = E::error_schemas();
272    error_schemas.append(&mut T::error_schemas());
273    error_schemas
274  }
275
276  fn responses(content_type: Option<String>) -> Option<Responses> {
277    let responses = T::responses(content_type.clone());
278    match responses {
279      None => E::responses(content_type),
280      Some(mut responses) => {
281        responses
282          .responses
283          .append(&mut E::responses(content_type).map(|r| r.responses).unwrap_or_default());
284        Some(responses)
285      }
286    }
287  }
288}
289
290#[cfg(feature = "actix")]
291impl<T> ApiComponent for actix_web::web::Data<T> {
292  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
293    vec![]
294  }
295
296  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
297    None
298  }
299}
300
301#[cfg(feature = "actix")]
302impl<T: Clone> ApiComponent for actix_web::web::ReqData<T> {
303  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
304    vec![]
305  }
306
307  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
308    None
309  }
310}
311
312#[cfg(feature = "actix")]
313impl<F, R, P> ApiComponent for ResponseWrapper<F, P>
314where
315  F: Future<Output = R>,
316  R: actix_web::Responder + ApiComponent,
317  P: PathItemDefinition,
318{
319  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
320    R::child_schemas()
321  }
322
323  fn raw_schema() -> Option<ReferenceOr<Schema>> {
324    R::raw_schema()
325  }
326
327  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
328    R::schema()
329  }
330
331  fn error_responses() -> Vec<(String, Response)> {
332    R::error_responses()
333  }
334
335  fn error_schemas() -> BTreeMap<String, (String, ReferenceOr<Schema>)> {
336    R::error_schemas()
337  }
338
339  fn responses(content_type: Option<String>) -> Option<Responses> {
340    let mut responses = vec![];
341    if let Some(response) = R::responses(content_type.clone()) {
342      responses.append(
343        &mut response
344          .responses
345          .into_iter()
346          .collect::<Vec<(String, ReferenceOr<Response>)>>(),
347      );
348    } else if let Some((name, schema)) = Self::schema() {
349      let ref_or = match schema {
350        r @ ReferenceOr::Reference { .. } => r,
351        ReferenceOr::Object(schema_obj) => {
352          let _ref = ReferenceOr::Reference {
353            _ref: format!("#/components/schemas/{}", name),
354          };
355          match schema_obj {
356            Schema::Object(obj) => {
357              if obj.instance_type == Some(SingleOrVec::Single(Box::new(InstanceType::Array))) {
358                ReferenceOr::Object(Schema::Object(obj))
359              } else {
360                _ref
361              }
362            }
363            Schema::Bool(_) => _ref,
364          }
365        }
366      };
367      responses.push((
368        "200".to_owned(),
369        ReferenceOr::Object(Response {
370          content: BTreeMap::from_iter(vec![(
371            content_type.unwrap_or_else(Self::content_type),
372            MediaType {
373              schema: Some(ref_or),
374              ..Default::default()
375            },
376          )]),
377          ..Default::default()
378        }),
379      ));
380    } else if let Some(schema) = Self::raw_schema() {
381      responses.push((
382        "200".to_owned(),
383        ReferenceOr::Object(Response {
384          content: BTreeMap::from_iter(vec![(
385            content_type.unwrap_or_else(Self::content_type),
386            MediaType {
387              schema: Some(schema),
388              ..Default::default()
389            },
390          )]),
391          ..Default::default()
392        }),
393      ));
394    } else if let Some(content_type) = content_type {
395      responses.push((
396        "200".to_owned(),
397        ReferenceOr::Object(Response {
398          content: BTreeMap::from_iter(vec![(content_type, MediaType::default())]),
399          ..Default::default()
400        }),
401      ));
402    } else {
403      responses.push(("200".to_owned(), ReferenceOr::Object(Response::default())));
404    }
405
406    responses.append(
407      &mut Self::error_responses()
408        .into_iter()
409        .map(|(status, schema)| (status, ReferenceOr::Object(schema)))
410        .collect(),
411    );
412    Some(Responses {
413      responses: BTreeMap::from_iter(responses),
414      ..Default::default()
415    })
416  }
417}
418
419#[cfg(test)]
420mod test {
421  use crate::ApiComponent;
422  use apistos_models::reference_or::ReferenceOr;
423  use assert_json_diff::assert_json_eq;
424  use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec};
425  use schemars::{Map, Set};
426  use serde_json::json;
427
428  #[test]
429  #[allow(dead_code)]
430  fn api_component_schema_vec() {
431    struct TestChild {
432      surname: String,
433    }
434
435    impl ApiComponent for TestChild {
436      fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
437        vec![]
438      }
439
440      fn schema() -> Option<(String, ReferenceOr<Schema>)> {
441        Some((
442          "TestChild".to_string(),
443          ReferenceOr::Object(Schema::Object(SchemaObject {
444            object: Some(Box::new(ObjectValidation {
445              required: Set::from_iter(vec!["surname".to_string()]),
446              properties: Map::from_iter(vec![(
447                "surname".to_string(),
448                Schema::Object(SchemaObject {
449                  instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
450                  ..Default::default()
451                }),
452              )]),
453              ..Default::default()
454            })),
455            ..Default::default()
456          })),
457        ))
458      }
459    }
460
461    struct Test {
462      name: String,
463      surname: TestChild,
464    }
465
466    impl ApiComponent for Test {
467      fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
468        <TestChild as ApiComponent>::schema().into_iter().collect()
469      }
470
471      fn schema() -> Option<(String, ReferenceOr<Schema>)> {
472        Some((
473          "Test".to_string(),
474          ReferenceOr::Object(Schema::Object(SchemaObject {
475            object: Some(Box::new(ObjectValidation {
476              required: Set::from_iter(vec!["name".to_string(), "surname".to_string()]),
477              properties: Map::from_iter(vec![
478                (
479                  "name".to_string(),
480                  Schema::Object(SchemaObject {
481                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
482                    ..Default::default()
483                  }),
484                ),
485                (
486                  "surname".to_string(),
487                  Schema::new_ref("#/components/schemas/TestChild".to_string()),
488                ),
489              ]),
490              ..Default::default()
491            })),
492            ..Default::default()
493          })),
494        ))
495      }
496    }
497
498    let schema = <Vec<Test> as ApiComponent>::schema();
499    assert!(schema.is_some());
500
501    let json = serde_json::to_value(schema.expect("Missing schema").1).expect("Unable to serialize as Json");
502    assert_json_eq!(
503      json,
504      json!({
505        "type": "array",
506        "items": {
507          "$ref": "#/components/schemas/Test"
508        }
509      })
510    );
511
512    let child_schema = <Vec<Test> as ApiComponent>::child_schemas();
513    assert_eq!(child_schema.len(), 2);
514    let first_child_schema = child_schema.first().cloned();
515    assert!(first_child_schema.is_some());
516
517    let json =
518      serde_json::to_value(first_child_schema.expect("Missing child schema").1).expect("Unable to serialize as Json");
519    assert_json_eq!(
520      json,
521      json!({
522        "properties": {
523          "name": {
524            "type": "string"
525          },
526          "surname": {
527            "$ref": "#/components/schemas/TestChild"
528          }
529        },
530        "required": [
531          "name",
532          "surname"
533        ]
534      })
535    );
536
537    let last_child_schema = child_schema.last().cloned();
538    assert!(last_child_schema.is_some());
539
540    let json =
541      serde_json::to_value(last_child_schema.expect("Missing child schema").1).expect("Unable to serialize as Json");
542    assert_json_eq!(
543      json,
544      json!( {
545        "properties": {
546          "surname": {
547            "type": "string"
548          }
549        },
550        "required": [
551          "surname"
552        ]
553      })
554    );
555  }
556}