1use crate::spec::*;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
8pub struct OpenApiBuilder {
9 spec: OpenApiSpec,
10}
11
12impl OpenApiBuilder {
13 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 pub fn description(mut self, description: impl Into<String>) -> Self {
37 self.spec.info.description = Some(description.into());
38 self
39 }
40
41 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 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 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 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 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 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 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 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 pub fn security(mut self, requirement: SecurityRequirement) -> Self {
109 self.spec.security.push(requirement);
110 self
111 }
112
113 pub fn build(self) -> OpenApiSpec {
115 self.spec
116 }
117}
118
119impl OpenApiBuilder {
121 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 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 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
163pub 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
211pub 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
274pub 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}