Skip to main content

armature_openapi/
spec.rs

1//! OpenAPI 3.0 specification types
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// OpenAPI 3.0 specification
7#[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/// API information
23#[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/// Contact information
38#[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/// License information
49#[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/// Server information
57#[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/// Path item
65#[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/// Operation (endpoint)
80#[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/// Parameter
100#[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/// Parameter location
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "lowercase")]
116pub enum ParameterLocation {
117    Query,
118    Header,
119    Path,
120    Cookie,
121}
122
123/// Request body
124#[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/// Media type
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MediaType {
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub schema: Option<Schema>,
138}
139
140/// Response
141#[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/// Schema (simplified)
149#[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/// Components
168#[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/// Security scheme
177#[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/// API key location
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(rename_all = "lowercase")]
205pub enum ApiKeyLocation {
206    Query,
207    Header,
208    Cookie,
209}
210
211/// OAuth flows
212#[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/// OAuth flow
225#[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
236/// Security requirement
237pub type SecurityRequirement = HashMap<String, Vec<String>>;
238
239/// Tag
240#[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}