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}