Skip to main content

armature_openapi/
builder.rs

1//! Builder for creating OpenAPI specifications programmatically
2
3use crate::spec::*;
4use std::collections::HashMap;
5
6/// Builder for OpenAPI specifications
7#[derive(Debug, Clone)]
8pub struct OpenApiBuilder {
9    spec: OpenApiSpec,
10}
11
12impl OpenApiBuilder {
13    /// Create a new OpenAPI builder
14    pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
15        Self {
16            spec: OpenApiSpec {
17                openapi: "3.0.0".to_string(),
18                info: Info {
19                    title: title.into(),
20                    version: version.into(),
21                    description: None,
22                    terms_of_service: None,
23                    contact: None,
24                    license: None,
25                },
26                servers: Vec::new(),
27                paths: HashMap::new(),
28                components: Some(Components::default()),
29                security: Vec::new(),
30                tags: Vec::new(),
31            },
32        }
33    }
34
35    /// Set description
36    pub fn description(mut self, description: impl Into<String>) -> Self {
37        self.spec.info.description = Some(description.into());
38        self
39    }
40
41    /// Set terms of service
42    pub fn terms_of_service(mut self, terms: impl Into<String>) -> Self {
43        self.spec.info.terms_of_service = Some(terms.into());
44        self
45    }
46
47    /// Add contact information
48    pub fn contact(
49        mut self,
50        name: Option<String>,
51        url: Option<String>,
52        email: Option<String>,
53    ) -> Self {
54        self.spec.info.contact = Some(Contact { name, url, email });
55        self
56    }
57
58    /// Add license
59    pub fn license(mut self, name: impl Into<String>, url: Option<String>) -> Self {
60        self.spec.info.license = Some(License {
61            name: name.into(),
62            url,
63        });
64        self
65    }
66
67    /// Add a server
68    pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
69        self.spec.servers.push(Server {
70            url: url.into(),
71            description,
72        });
73        self
74    }
75
76    /// Add a tag
77    pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
78        self.spec.tags.push(Tag {
79            name: name.into(),
80            description,
81        });
82        self
83    }
84
85    /// Add a path
86    pub fn path(mut self, path: impl Into<String>, item: PathItem) -> Self {
87        self.spec.paths.insert(path.into(), item);
88        self
89    }
90
91    /// Add a schema component
92    pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
93        if let Some(ref mut components) = self.spec.components {
94            components.schemas.insert(name.into(), schema);
95        }
96        self
97    }
98
99    /// Add a security scheme
100    pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
101        if let Some(ref mut components) = self.spec.components {
102            components.security_schemes.insert(name.into(), scheme);
103        }
104        self
105    }
106
107    /// Add global security requirement
108    pub fn security(mut self, requirement: SecurityRequirement) -> Self {
109        self.spec.security.push(requirement);
110        self
111    }
112
113    /// Build the OpenAPI specification
114    pub fn build(self) -> OpenApiSpec {
115        self.spec
116    }
117}
118
119/// Helper functions for creating common components
120impl OpenApiBuilder {
121    /// Add Bearer JWT authentication
122    pub fn add_bearer_auth(self, name: impl Into<String>) -> Self {
123        self.security_scheme(
124            name,
125            SecurityScheme::Http {
126                scheme: "bearer".to_string(),
127                bearer_format: Some("JWT".to_string()),
128            },
129        )
130    }
131
132    /// Add API key authentication
133    pub fn add_api_key_auth(
134        self,
135        name: impl Into<String>,
136        key_name: impl Into<String>,
137        location: ApiKeyLocation,
138    ) -> Self {
139        self.security_scheme(
140            name,
141            SecurityScheme::ApiKey {
142                name: key_name.into(),
143                location,
144            },
145        )
146    }
147
148    /// Add OpenID Connect authentication
149    pub fn add_openid_connect_auth(
150        self,
151        name: impl Into<String>,
152        open_id_connect_url: impl Into<String>,
153    ) -> Self {
154        self.security_scheme(
155            name,
156            SecurityScheme::OpenIdConnect {
157                open_id_connect_url: open_id_connect_url.into(),
158            },
159        )
160    }
161}
162
163/// Builder for path items
164pub struct PathItemBuilder {
165    item: PathItem,
166}
167
168impl PathItemBuilder {
169    pub fn new() -> Self {
170        Self {
171            item: PathItem::default(),
172        }
173    }
174
175    pub fn get(mut self, operation: Operation) -> Self {
176        self.item.get = Some(operation);
177        self
178    }
179
180    pub fn post(mut self, operation: Operation) -> Self {
181        self.item.post = Some(operation);
182        self
183    }
184
185    pub fn put(mut self, operation: Operation) -> Self {
186        self.item.put = Some(operation);
187        self
188    }
189
190    pub fn delete(mut self, operation: Operation) -> Self {
191        self.item.delete = Some(operation);
192        self
193    }
194
195    pub fn patch(mut self, operation: Operation) -> Self {
196        self.item.patch = Some(operation);
197        self
198    }
199
200    pub fn build(self) -> PathItem {
201        self.item
202    }
203}
204
205impl Default for PathItemBuilder {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211/// Builder for operations
212pub struct OperationBuilder {
213    operation: Operation,
214}
215
216impl OperationBuilder {
217    pub fn new() -> Self {
218        Self {
219            operation: Operation::default(),
220        }
221    }
222
223    pub fn summary(mut self, summary: impl Into<String>) -> Self {
224        self.operation.summary = Some(summary.into());
225        self
226    }
227
228    pub fn description(mut self, description: impl Into<String>) -> Self {
229        self.operation.description = Some(description.into());
230        self
231    }
232
233    pub fn operation_id(mut self, id: impl Into<String>) -> Self {
234        self.operation.operation_id = Some(id.into());
235        self
236    }
237
238    pub fn tag(mut self, tag: impl Into<String>) -> Self {
239        self.operation.tags.push(tag.into());
240        self
241    }
242
243    pub fn parameter(mut self, parameter: Parameter) -> Self {
244        self.operation.parameters.push(parameter);
245        self
246    }
247
248    pub fn request_body(mut self, body: RequestBody) -> Self {
249        self.operation.request_body = Some(body);
250        self
251    }
252
253    pub fn response(mut self, status: impl Into<String>, response: Response) -> Self {
254        self.operation.responses.insert(status.into(), response);
255        self
256    }
257
258    pub fn security(mut self, requirement: SecurityRequirement) -> Self {
259        self.operation.security.push(requirement);
260        self
261    }
262
263    pub fn build(self) -> Operation {
264        self.operation
265    }
266}
267
268impl Default for OperationBuilder {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274/// Helper functions for creating schemas
275pub fn string_schema() -> Schema {
276    Schema {
277        schema_type: Some("string".to_string()),
278        ..Default::default()
279    }
280}
281
282pub fn integer_schema() -> Schema {
283    Schema {
284        schema_type: Some("integer".to_string()),
285        format: Some("int64".to_string()),
286        ..Default::default()
287    }
288}
289
290pub fn number_schema() -> Schema {
291    Schema {
292        schema_type: Some("number".to_string()),
293        format: Some("double".to_string()),
294        ..Default::default()
295    }
296}
297
298pub fn boolean_schema() -> Schema {
299    Schema {
300        schema_type: Some("boolean".to_string()),
301        ..Default::default()
302    }
303}
304
305pub fn array_schema(items: Schema) -> Schema {
306    Schema {
307        schema_type: Some("array".to_string()),
308        items: Some(Box::new(items)),
309        ..Default::default()
310    }
311}
312
313pub fn object_schema(properties: HashMap<String, Schema>, required: Vec<String>) -> Schema {
314    Schema {
315        schema_type: Some("object".to_string()),
316        properties: Some(properties),
317        required,
318        ..Default::default()
319    }
320}
321
322pub fn ref_schema(reference: impl Into<String>) -> Schema {
323    Schema {
324        reference: Some(format!("#/components/schemas/{}", reference.into())),
325        ..Default::default()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_openapi_builder_basic() {
335        let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
336
337        assert_eq!(spec.info.title, "Test API");
338        assert_eq!(spec.info.version, "1.0.0");
339        assert_eq!(spec.openapi, "3.0.0");
340    }
341
342    #[test]
343    fn test_openapi_builder_with_description() {
344        let spec = OpenApiBuilder::new("Test API", "1.0.0")
345            .description("A test API")
346            .build();
347
348        assert_eq!(spec.info.description, Some("A test API".to_string()));
349    }
350
351    #[test]
352    fn test_operation_builder_basic() {
353        let operation = OperationBuilder::new()
354            .summary("Get user")
355            .description("Get a user by ID")
356            .operation_id("getUser")
357            .build();
358
359        assert_eq!(operation.summary, Some("Get user".to_string()));
360        assert_eq!(operation.description, Some("Get a user by ID".to_string()));
361        assert_eq!(operation.operation_id, Some("getUser".to_string()));
362    }
363
364    #[test]
365    fn test_operation_builder_with_tag() {
366        let operation = OperationBuilder::new().tag("users").tag("admin").build();
367
368        assert_eq!(operation.tags.len(), 2);
369        assert!(operation.tags.contains(&"users".to_string()));
370        assert!(operation.tags.contains(&"admin".to_string()));
371    }
372
373    #[test]
374    fn test_string_schema() {
375        let schema = string_schema();
376        assert_eq!(schema.schema_type, Some("string".to_string()));
377    }
378
379    #[test]
380    fn test_integer_schema() {
381        let schema = integer_schema();
382        assert_eq!(schema.schema_type, Some("integer".to_string()));
383        assert_eq!(schema.format, Some("int64".to_string()));
384    }
385
386    #[test]
387    fn test_number_schema() {
388        let schema = number_schema();
389        assert_eq!(schema.schema_type, Some("number".to_string()));
390        assert_eq!(schema.format, Some("double".to_string()));
391    }
392
393    #[test]
394    fn test_boolean_schema() {
395        let schema = boolean_schema();
396        assert_eq!(schema.schema_type, Some("boolean".to_string()));
397    }
398
399    #[test]
400    fn test_array_schema() {
401        let items = string_schema();
402        let schema = array_schema(items);
403
404        assert_eq!(schema.schema_type, Some("array".to_string()));
405        assert!(schema.items.is_some());
406    }
407
408    #[test]
409    fn test_object_schema() {
410        let mut properties = HashMap::new();
411        properties.insert("name".to_string(), string_schema());
412        properties.insert("age".to_string(), integer_schema());
413
414        let required = vec!["name".to_string()];
415        let schema = object_schema(properties, required);
416
417        assert_eq!(schema.schema_type, Some("object".to_string()));
418        assert_eq!(schema.properties.as_ref().unwrap().len(), 2);
419        assert_eq!(schema.required.len(), 1);
420    }
421
422    #[test]
423    fn test_ref_schema() {
424        let schema = ref_schema("User");
425        assert_eq!(
426            schema.reference,
427            Some("#/components/schemas/User".to_string())
428        );
429    }
430
431    #[test]
432    fn test_operation_builder_default() {
433        let builder = OperationBuilder::default();
434        let operation = builder.build();
435        assert!(operation.summary.is_none());
436    }
437
438    #[test]
439    fn test_add_openid_connect_auth() {
440        let spec = OpenApiBuilder::new("Test API", "1.0.0")
441            .add_openid_connect_auth(
442                "oidc",
443                "https://example.com/.well-known/openid-configuration",
444            )
445            .build();
446
447        let components = spec.components.unwrap();
448        match components.security_schemes.get("oidc").unwrap() {
449            SecurityScheme::OpenIdConnect {
450                open_id_connect_url,
451            } => {
452                assert_eq!(
453                    open_id_connect_url,
454                    "https://example.com/.well-known/openid-configuration"
455                );
456            }
457            _ => panic!("expected OpenIdConnect variant"),
458        }
459    }
460}