1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct OpenApiSpec {
9 pub openapi: String,
10 pub info: Info,
11 #[serde(skip_serializing_if = "Vec::is_empty", default)]
12 pub servers: Vec<Server>,
13 pub paths: HashMap<String, PathItem>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub components: Option<Components>,
16 #[serde(skip_serializing_if = "Vec::is_empty", default)]
17 pub security: Vec<SecurityRequirement>,
18 #[serde(skip_serializing_if = "Vec::is_empty", default)]
19 pub tags: Vec<Tag>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Info {
25 pub title: String,
26 pub version: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub description: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub terms_of_service: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub contact: Option<Contact>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub license: Option<License>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Contact {
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub name: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub url: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub email: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct License {
51 pub name: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub url: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Server {
59 pub url: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub description: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66pub struct PathItem {
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub get: Option<Operation>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub post: Option<Operation>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub put: Option<Operation>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub delete: Option<Operation>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub patch: Option<Operation>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct Operation {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub summary: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub description: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub operation_id: Option<String>,
88 #[serde(skip_serializing_if = "Vec::is_empty", default)]
89 pub tags: Vec<String>,
90 #[serde(skip_serializing_if = "Vec::is_empty", default)]
91 pub parameters: Vec<Parameter>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub request_body: Option<RequestBody>,
94 pub responses: HashMap<String, Response>,
95 #[serde(skip_serializing_if = "Vec::is_empty", default)]
96 pub security: Vec<SecurityRequirement>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Parameter {
102 pub name: String,
103 #[serde(rename = "in")]
104 pub location: ParameterLocation,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub description: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub required: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub schema: Option<Schema>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "lowercase")]
116pub enum ParameterLocation {
117 Query,
118 Header,
119 Path,
120 Cookie,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct RequestBody {
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub description: Option<String>,
128 pub content: HashMap<String, MediaType>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub required: Option<bool>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MediaType {
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub schema: Option<Schema>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct Response {
143 pub description: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub content: Option<HashMap<String, MediaType>>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, Default)]
150pub struct Schema {
151 #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
152 pub schema_type: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub format: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub description: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub properties: Option<HashMap<String, Schema>>,
159 #[serde(skip_serializing_if = "Vec::is_empty", default)]
160 pub required: Vec<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub items: Option<Box<Schema>>,
163 #[serde(skip_serializing_if = "Option::is_none", rename = "$ref")]
164 pub reference: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, Default)]
169pub struct Components {
170 #[serde(skip_serializing_if = "HashMap::is_empty", default)]
171 pub schemas: HashMap<String, Schema>,
172 #[serde(skip_serializing_if = "HashMap::is_empty", default)]
173 pub security_schemes: HashMap<String, SecurityScheme>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(tag = "type", rename_all = "lowercase")]
179#[non_exhaustive]
180pub enum SecurityScheme {
181 #[serde(rename = "http")]
182 Http {
183 scheme: String,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 bearer_format: Option<String>,
186 },
187 #[serde(rename = "apiKey")]
188 ApiKey {
189 name: String,
190 #[serde(rename = "in")]
191 location: ApiKeyLocation,
192 },
193 #[serde(rename = "oauth2")]
194 OAuth2 { flows: Box<OAuthFlows> },
195 #[serde(rename = "openIdConnect")]
196 OpenIdConnect {
197 #[serde(rename = "openIdConnectUrl")]
198 open_id_connect_url: String,
199 },
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(rename_all = "lowercase")]
205pub enum ApiKeyLocation {
206 Query,
207 Header,
208 Cookie,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, Default)]
213pub struct OAuthFlows {
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub implicit: Option<OAuthFlow>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub password: Option<OAuthFlow>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub client_credentials: Option<OAuthFlow>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub authorization_code: Option<OAuthFlow>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct OAuthFlow {
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub authorization_url: Option<String>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub token_url: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub refresh_url: Option<String>,
233 pub scopes: HashMap<String, String>,
234}
235
236pub type SecurityRequirement = HashMap<String, Vec<String>>;
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Tag {
242 pub name: String,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub description: Option<String>,
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_openapi_spec_creation() {
253 let spec = OpenApiSpec {
254 openapi: "3.0.0".to_string(),
255 info: Info {
256 title: "Test".to_string(),
257 version: "1.0.0".to_string(),
258 description: None,
259 terms_of_service: None,
260 contact: None,
261 license: None,
262 },
263 paths: HashMap::new(),
264 components: None,
265 servers: Vec::new(),
266 security: Vec::new(),
267 tags: Vec::new(),
268 };
269
270 assert_eq!(spec.openapi, "3.0.0");
271 }
272
273 #[test]
274 fn test_info_creation() {
275 let info = Info {
276 title: "Test API".to_string(),
277 version: "1.0.0".to_string(),
278 description: None,
279 terms_of_service: None,
280 contact: None,
281 license: None,
282 };
283 assert!(!info.title.is_empty());
284 assert!(!info.version.is_empty());
285 }
286
287 #[test]
288 fn test_path_item_creation() {
289 let path = PathItem {
290 get: Some(Operation::default()),
291 ..Default::default()
292 };
293
294 assert!(path.get.is_some());
295 assert!(path.post.is_none());
296 }
297
298 #[test]
299 fn test_schema_string_type() {
300 let schema = Schema {
301 schema_type: Some("string".to_string()),
302 format: Some("email".to_string()),
303 ..Default::default()
304 };
305
306 assert_eq!(schema.schema_type, Some("string".to_string()));
307 assert_eq!(schema.format, Some("email".to_string()));
308 }
309
310 #[test]
311 fn test_schema_object_type() {
312 let mut properties = HashMap::new();
313 properties.insert(
314 "name".to_string(),
315 Schema {
316 schema_type: Some("string".to_string()),
317 ..Default::default()
318 },
319 );
320
321 let schema = Schema {
322 schema_type: Some("object".to_string()),
323 properties: Some(properties),
324 ..Default::default()
325 };
326
327 assert_eq!(schema.schema_type, Some("object".to_string()));
328 assert!(schema.properties.is_some());
329 assert_eq!(schema.properties.as_ref().unwrap().len(), 1);
330 }
331
332 #[test]
333 fn test_schema_array_type() {
334 let schema = Schema {
335 schema_type: Some("array".to_string()),
336 items: Some(Box::new(Schema {
337 schema_type: Some("string".to_string()),
338 ..Default::default()
339 })),
340 ..Default::default()
341 };
342
343 assert_eq!(schema.schema_type, Some("array".to_string()));
344 assert!(schema.items.is_some());
345 }
346
347 #[test]
348 fn test_media_type_with_schema() {
349 let media_type = MediaType {
350 schema: Some(Schema {
351 schema_type: Some("object".to_string()),
352 ..Default::default()
353 }),
354 };
355
356 assert!(media_type.schema.is_some());
357 }
358
359 #[test]
360 fn test_request_body() {
361 let mut content = HashMap::new();
362 content.insert(
363 "application/json".to_string(),
364 MediaType {
365 schema: Some(Schema::default()),
366 },
367 );
368
369 let request_body = RequestBody {
370 description: Some("User data".to_string()),
371 content,
372 required: Some(true),
373 };
374
375 assert!(request_body.required.unwrap());
376 assert_eq!(request_body.content.len(), 1);
377 }
378
379 #[test]
380 fn test_tag_creation() {
381 let tag = Tag {
382 name: "users".to_string(),
383 description: Some("User operations".to_string()),
384 };
385
386 assert_eq!(tag.name, "users");
387 assert!(tag.description.is_some());
388 }
389
390 #[test]
391 fn test_components() {
392 let mut schemas = HashMap::new();
393 schemas.insert("User".to_string(), Schema::default());
394
395 let components = Components {
396 schemas,
397 security_schemes: HashMap::new(),
398 };
399
400 assert!(!components.schemas.is_empty());
401 assert_eq!(components.schemas.len(), 1);
402 }
403
404 #[test]
405 fn test_schema_serialization() {
406 let schema = Schema {
407 schema_type: Some("string".to_string()),
408 format: Some("email".to_string()),
409 ..Default::default()
410 };
411
412 let json = serde_json::to_string(&schema).unwrap();
413 let deserialized: Schema = serde_json::from_str(&json).unwrap();
414
415 assert_eq!(schema.schema_type, deserialized.schema_type);
416 assert_eq!(schema.format, deserialized.format);
417 }
418
419 #[test]
420 fn test_operation_with_tags() {
421 let operation = Operation {
422 tags: vec!["users".to_string(), "admin".to_string()],
423 ..Default::default()
424 };
425
426 assert_eq!(operation.tags.len(), 2);
427 assert!(operation.tags.contains(&"users".to_string()));
428 }
429
430 #[test]
431 fn test_schema_with_ref() {
432 let schema = Schema {
433 reference: Some("#/components/schemas/User".to_string()),
434 ..Default::default()
435 };
436
437 assert!(schema.reference.is_some());
438 assert!(schema.schema_type.is_none());
439 }
440
441 #[test]
442 fn test_response_creation() {
443 let response = Response {
444 description: "Success".to_string(),
445 content: Some(HashMap::new()),
446 };
447
448 assert_eq!(response.description, "Success");
449 assert!(response.content.is_some());
450 }
451
452 #[test]
453 fn test_schema_defaults() {
454 let schema = Schema::default();
455 assert!(schema.schema_type.is_none());
456 assert!(schema.format.is_none());
457 assert!(schema.properties.is_none());
458 }
459
460 #[test]
461 fn test_security_scheme_open_id_connect_serialization() {
462 let scheme = SecurityScheme::OpenIdConnect {
463 open_id_connect_url: "https://example.com/.well-known/openid-configuration".to_string(),
464 };
465
466 let json = serde_json::to_value(&scheme).unwrap();
467 assert_eq!(json["type"], "openIdConnect");
468 assert_eq!(
469 json["openIdConnectUrl"],
470 "https://example.com/.well-known/openid-configuration"
471 );
472
473 let deserialized: SecurityScheme = serde_json::from_value(json).unwrap();
474 match deserialized {
475 SecurityScheme::OpenIdConnect {
476 open_id_connect_url,
477 } => {
478 assert_eq!(
479 open_id_connect_url,
480 "https://example.com/.well-known/openid-configuration"
481 );
482 }
483 _ => panic!("expected OpenIdConnect variant"),
484 }
485 }
486}