Skip to main content

nestforge_core/
documentation.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{json, Value};
3
4#[derive(Debug, Clone, Serialize, Deserialize, Default)]
5pub struct OpenApiSchemaComponent {
6    pub name: String,
7    pub schema: Value,
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct RouteResponseDocumentation {
12    pub status: u16,
13    pub description: String,
14    pub schema: Option<Value>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18pub struct RouteDocumentation {
19    pub method: String,
20    pub path: String,
21    pub summary: Option<String>,
22    pub description: Option<String>,
23    pub tags: Vec<String>,
24    pub responses: Vec<RouteResponseDocumentation>,
25    pub requires_auth: bool,
26    pub required_roles: Vec<String>,
27    pub request_body: Option<Value>,
28    pub schema_components: Vec<OpenApiSchemaComponent>,
29}
30
31impl RouteDocumentation {
32    pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
33        Self {
34            method: method.into(),
35            path: path.into(),
36            summary: None,
37            description: None,
38            tags: Vec::new(),
39            responses: vec![RouteResponseDocumentation {
40                status: 200,
41                description: "OK".to_string(),
42                schema: None,
43            }],
44            requires_auth: false,
45            required_roles: Vec::new(),
46            request_body: None,
47            schema_components: Vec::new(),
48        }
49    }
50
51    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
52        self.summary = Some(summary.into());
53        self
54    }
55
56    pub fn with_description(mut self, description: impl Into<String>) -> Self {
57        self.description = Some(description.into());
58        self
59    }
60
61    pub fn with_tags<I, S>(mut self, tags: I) -> Self
62    where
63        I: IntoIterator<Item = S>,
64        S: Into<String>,
65    {
66        self.tags = tags.into_iter().map(Into::into).collect();
67        self
68    }
69
70    pub fn with_responses(mut self, responses: Vec<RouteResponseDocumentation>) -> Self {
71        self.responses = responses;
72        self
73    }
74
75    pub fn with_request_body_schema(mut self, schema: Value) -> Self {
76        self.request_body = Some(schema);
77        self
78    }
79
80    pub fn with_success_response_schema(mut self, schema: Value) -> Self {
81        if let Some(response) = self
82            .responses
83            .iter_mut()
84            .find(|response| (200..300).contains(&response.status))
85        {
86            response.schema = Some(schema);
87            return self;
88        }
89
90        if let Some(response) = self.responses.first_mut() {
91            response.schema = Some(schema);
92            return self;
93        }
94
95        self.responses.push(RouteResponseDocumentation {
96            status: 200,
97            description: "OK".to_string(),
98            schema: Some(schema),
99        });
100        self
101    }
102
103    pub fn with_schema_components<I>(mut self, components: I) -> Self
104    where
105        I: IntoIterator<Item = OpenApiSchemaComponent>,
106    {
107        for component in components {
108            if !self
109                .schema_components
110                .iter()
111                .any(|existing| existing.name == component.name)
112            {
113                self.schema_components.push(component);
114            }
115        }
116
117        self
118    }
119
120    pub fn requires_auth(mut self) -> Self {
121        self.requires_auth = true;
122        self
123    }
124
125    pub fn with_required_roles<I, S>(mut self, roles: I) -> Self
126    where
127        I: IntoIterator<Item = S>,
128        S: Into<String>,
129    {
130        self.required_roles = roles.into_iter().map(Into::into).collect();
131        self
132    }
133}
134
135pub trait DocumentedController: Send + Sync + 'static {
136    fn route_docs() -> Vec<RouteDocumentation> {
137        Vec::new()
138    }
139}
140
141pub trait OpenApiSchema {
142    fn schema_name() -> Option<&'static str> {
143        None
144    }
145
146    fn schema() -> Value;
147
148    fn schema_or_ref() -> Value {
149        match Self::schema_name() {
150            Some(name) => json!({ "$ref": format!("#/components/schemas/{name}") }),
151            None => Self::schema(),
152        }
153    }
154
155    fn schema_components() -> Vec<OpenApiSchemaComponent> {
156        match Self::schema_name() {
157            Some(name) => vec![OpenApiSchemaComponent {
158                name: name.to_string(),
159                schema: Self::schema(),
160            }],
161            None => Vec::new(),
162        }
163    }
164}
165
166pub fn openapi_schema_for<T: OpenApiSchema>() -> Value {
167    T::schema_or_ref()
168}
169
170pub fn openapi_schema_components_for<T: OpenApiSchema>() -> Vec<OpenApiSchemaComponent> {
171    T::schema_components()
172}
173
174pub fn openapi_array_schema_for<T: OpenApiSchema>() -> Value {
175    json!({
176        "type": "array",
177        "items": T::schema_or_ref(),
178    })
179}
180
181pub fn openapi_nullable_schema_for<T: OpenApiSchema>() -> Value {
182    json!({
183        "anyOf": [
184            T::schema_or_ref(),
185            { "type": "null" }
186        ]
187    })
188}
189
190macro_rules! primitive_openapi_schema {
191    ($($ty:ty => $schema:expr),* $(,)?) => {
192        $(
193            impl OpenApiSchema for $ty {
194                fn schema() -> Value {
195                    $schema
196                }
197            }
198        )*
199    };
200}
201
202primitive_openapi_schema!(
203    String => json!({ "type": "string" }),
204    bool => json!({ "type": "boolean" }),
205    u8 => json!({ "type": "integer", "format": "uint8" }),
206    u16 => json!({ "type": "integer", "format": "uint16" }),
207    u32 => json!({ "type": "integer", "format": "uint32" }),
208    u64 => json!({ "type": "integer", "format": "uint64" }),
209    usize => json!({ "type": "integer", "format": "uint" }),
210    i8 => json!({ "type": "integer", "format": "int8" }),
211    i16 => json!({ "type": "integer", "format": "int16" }),
212    i32 => json!({ "type": "integer", "format": "int32" }),
213    i64 => json!({ "type": "integer", "format": "int64" }),
214    isize => json!({ "type": "integer", "format": "int" }),
215    f32 => json!({ "type": "number", "format": "float" }),
216    f64 => json!({ "type": "number", "format": "double" })
217);
218
219impl<T> OpenApiSchema for Vec<T>
220where
221    T: OpenApiSchema,
222{
223    fn schema() -> Value {
224        openapi_array_schema_for::<T>()
225    }
226
227    fn schema_components() -> Vec<OpenApiSchemaComponent> {
228        T::schema_components()
229    }
230}
231
232impl<T> OpenApiSchema for Option<T>
233where
234    T: OpenApiSchema,
235{
236    fn schema() -> Value {
237        openapi_nullable_schema_for::<T>()
238    }
239
240    fn schema_components() -> Vec<OpenApiSchemaComponent> {
241        T::schema_components()
242    }
243}