Skip to main content

aster_forge_api/response/
schema.rs

1//! `OpenAPI` composition for envelopes, including typed unit values and nested schema collection.
2use super::{ApiErrorInfo, ApiResponse};
3use std::borrow::Cow;
4use utoipa::{
5    PartialSchema, ToSchema,
6    openapi::{
7        Ref, RefOr,
8        schema::{ObjectBuilder, Schema, Type},
9    },
10};
11
12// Utoipa's derived unit schema allows arbitrary values and adds a null default, which SDK
13// generators turn into a required field. Serde's unit is exactly null and never supplies a default.
14fn value_schema<T: ToSchema>() -> RefOr<Schema> {
15    if std::any::type_name::<T>() == "()" {
16        ObjectBuilder::new().schema_type(Type::Null).into()
17    } else {
18        T::schema()
19    }
20}
21
22fn collect<T: ToSchema>(schemas: &mut Vec<(String, RefOr<Schema>)>) {
23    schemas.push((T::name().into_owned(), value_schema::<T>()));
24    T::schemas(schemas);
25}
26
27fn collect_error_info<D: ToSchema>(schemas: &mut Vec<(String, RefOr<Schema>)>) {
28    schemas.push((
29        format!("ApiErrorInfo_{}", D::name()),
30        ApiErrorInfo::<D>::schema(),
31    ));
32    D::schemas(schemas);
33    if std::any::type_name::<D>() != "()" {
34        let diagnostic_name = D::name().into_owned();
35        if !schemas.iter().any(|(name, _)| name == &diagnostic_name) {
36            schemas.push((diagnostic_name, value_schema::<D>()));
37        }
38    }
39}
40
41fn diagnostic_schema<D: ToSchema>() -> RefOr<Schema> {
42    if std::any::type_name::<D>() == "()" {
43        value_schema::<D>()
44    } else {
45        RefOr::Ref(Ref::from_schema_name(D::name()))
46    }
47}
48
49impl<D: ToSchema> ToSchema for ApiErrorInfo<D> {
50    fn name() -> Cow<'static, str> {
51        Cow::Borrowed("ApiErrorInfo")
52    }
53    fn schemas(schemas: &mut Vec<(String, RefOr<Schema>)>) {
54        D::schemas(schemas);
55    }
56}
57
58// ComposeSchema is the integration point used by Utoipa's generic derives. Keep this dependency
59// detail private to this module; consumers use the usual ToSchema and full generic type syntax.
60impl<D: ToSchema> utoipa::__dev::ComposeSchema for ApiErrorInfo<D> {
61    fn compose(_generics: Vec<RefOr<Schema>>) -> RefOr<Schema> {
62        ObjectBuilder::new()
63            .property("retryable", bool::schema())
64            .required("retryable")
65            .property("diagnostic", diagnostic_schema::<D>())
66            .into()
67    }
68}
69
70impl<T: ToSchema, C: ToSchema, D: ToSchema> ToSchema for ApiResponse<T, C, D> {
71    fn name() -> Cow<'static, str> {
72        Cow::Borrowed("ApiResponse")
73    }
74    fn schemas(schemas: &mut Vec<(String, RefOr<Schema>)>) {
75        T::schemas(schemas);
76        collect::<C>(schemas);
77        collect_error_info::<D>(schemas);
78    }
79}
80
81impl<T: ToSchema, C: ToSchema, D: ToSchema> utoipa::__dev::ComposeSchema for ApiResponse<T, C, D> {
82    fn compose(_generics: Vec<RefOr<Schema>>) -> RefOr<Schema> {
83        ObjectBuilder::new()
84            .property("code", Ref::from_schema_name(C::name()))
85            .required("code")
86            .property("msg", String::schema())
87            .required("msg")
88            .property("data", value_schema::<T>())
89            .property(
90                "error",
91                Ref::from_schema_name(format!("ApiErrorInfo_{}", D::name())),
92            )
93            .into()
94    }
95}