1use serde_json::{json, Map, Value};
8
9mod paths;
10mod references;
11
12pub fn convert_swagger_to_openapi3(swagger: &Value) -> Result<Value, String> {
22 if swagger.get("swagger").and_then(|v| v.as_str()) != Some("2.0") {
24 return Err("Not a Swagger 2.0 specification".to_string());
25 }
26
27 let expanded = paths::expand(swagger)?;
28 let swagger = &expanded;
29 let mut openapi = Map::new();
30
31 openapi.insert("openapi".to_string(), json!("3.0.3"));
33
34 copy_extensions(swagger.as_object().expect("Swagger version requires an object"), &mut openapi);
35
36 if let Some(info) = swagger.get("info") {
38 openapi.insert("info".to_string(), info.clone());
39 }
40
41 let servers = convert_servers(swagger);
43 if !servers.is_empty() {
44 openapi.insert("servers".to_string(), json!(servers));
45 }
46
47 if let Some(tags) = swagger.get("tags") {
49 openapi.insert("tags".to_string(), tags.clone());
50 }
51
52 if let Some(paths) = swagger.get("paths") {
54 let global_consumes =
55 swagger.get("consumes").and_then(|v| v.as_array()).cloned().unwrap_or_default();
56 let global_produces =
57 swagger.get("produces").and_then(|v| v.as_array()).cloned().unwrap_or_default();
58
59 let converted_paths = convert_paths(paths, &global_consumes, &global_produces);
60 openapi.insert("paths".to_string(), converted_paths);
61 }
62
63 let mut components = Map::new();
65
66 if let Some(definitions) = swagger.get("definitions") {
68 let schemas = definitions.as_object().map(|definitions| {
69 definitions
70 .iter()
71 .map(|(name, schema)| (name.clone(), convert_schema_refs(schema)))
72 .collect::<Map<_, _>>()
73 });
74 components.insert(
75 "schemas".to_string(),
76 schemas.map(Value::Object).unwrap_or_else(|| definitions.clone()),
77 );
78 }
79
80 if let Some(security_defs) = swagger.get("securityDefinitions") {
82 let converted = convert_security_definitions(security_defs);
83 components.insert("securitySchemes".to_string(), converted);
84 }
85
86 if !components.is_empty() {
87 openapi.insert("components".to_string(), json!(components));
88 }
89
90 if let Some(security) = swagger.get("security") {
92 openapi.insert("security".to_string(), security.clone());
93 }
94
95 if let Some(external_docs) = swagger.get("externalDocs") {
97 openapi.insert("externalDocs".to_string(), external_docs.clone());
98 }
99
100 Ok(Value::Object(openapi))
101}
102
103fn convert_servers(swagger: &Value) -> Vec<Value> {
105 let host = swagger.get("host").and_then(|v| v.as_str());
106 let base_path = swagger.get("basePath").and_then(|v| v.as_str()).unwrap_or("");
107 let schemes = swagger
108 .get("schemes")
109 .and_then(|v| v.as_array())
110 .cloned()
111 .unwrap_or_else(|| vec![json!("https")]);
112
113 if let Some(host) = host {
114 schemes
115 .iter()
116 .filter_map(|scheme| {
117 scheme.as_str().map(|s| {
118 json!({
119 "url": format!("{}://{}{}", s, host, base_path)
120 })
121 })
122 })
123 .collect()
124 } else {
125 vec![json!({
127 "url": if base_path.is_empty() { "/" } else { base_path }
128 })]
129 }
130}
131
132fn convert_paths(paths: &Value, global_consumes: &[Value], global_produces: &[Value]) -> Value {
134 let Some(paths_obj) = paths.as_object() else {
135 return paths.clone();
136 };
137
138 let mut converted = Map::new();
139
140 for (path, path_item) in paths_obj {
141 if let Some(path_item_obj) = path_item.as_object() {
142 let converted_path_item =
143 convert_path_item(path_item_obj, global_consumes, global_produces);
144 converted.insert(path.clone(), Value::Object(converted_path_item));
145 }
146 }
147
148 Value::Object(converted)
149}
150
151fn convert_path_item(
153 path_item: &Map<String, Value>,
154 global_consumes: &[Value],
155 global_produces: &[Value],
156) -> Map<String, Value> {
157 let mut converted = Map::new();
158
159 for (key, value) in path_item {
160 match key.as_str() {
161 "get" | "post" | "put" | "delete" | "patch" | "head" | "options" => {
162 if let Some(op) = value.as_object() {
163 let converted_op = convert_operation(op, global_consumes, global_produces);
164 converted.insert(key.clone(), Value::Object(converted_op));
165 }
166 }
167 "parameters" => {
168 if let Some(params) = value.as_array() {
170 let converted_params: Vec<Value> =
171 params.iter().map(convert_parameter).collect();
172 converted.insert(key.clone(), json!(converted_params));
173 }
174 }
175 "$ref" => {
176 if let Some(ref_str) = value.as_str() {
178 converted.insert(key.clone(), json!(convert_ref(ref_str)));
179 }
180 }
181 _ => {
182 converted.insert(key.clone(), value.clone());
184 }
185 }
186 }
187
188 converted
189}
190
191fn convert_operation(
193 operation: &Map<String, Value>,
194 global_consumes: &[Value],
195 global_produces: &[Value],
196) -> Map<String, Value> {
197 let mut converted = Map::new();
198
199 let consumes: Vec<String> = operation
201 .get("consumes")
202 .and_then(|v| v.as_array())
203 .map(|arr| arr.iter())
204 .unwrap_or_else(|| global_consumes.iter())
205 .filter_map(|v| v.as_str().map(String::from))
206 .collect::<Vec<_>>();
207 let consumes = if consumes.is_empty() {
208 vec!["application/json".to_string()]
209 } else {
210 consumes
211 };
212
213 let produces: Vec<String> = operation
214 .get("produces")
215 .and_then(|v| v.as_array())
216 .map(|arr| arr.iter())
217 .unwrap_or_else(|| global_produces.iter())
218 .filter_map(|v| v.as_str().map(String::from))
219 .collect::<Vec<_>>();
220 let produces = if produces.is_empty() {
221 vec!["application/json".to_string()]
222 } else {
223 produces
224 };
225
226 let mut non_body_params = Vec::new();
228 let mut body_param: Option<&Value> = None;
229 let mut form_data_params = Vec::new();
230
231 if let Some(params) = operation.get("parameters").and_then(|v| v.as_array()) {
232 for param in params {
233 match param.get("in").and_then(|v| v.as_str()) {
234 Some("body") => body_param = Some(param),
235 Some("formData") => form_data_params.push(param),
236 _ => non_body_params.push(convert_parameter(param)),
237 }
238 }
239 }
240
241 if !non_body_params.is_empty() {
243 converted.insert("parameters".to_string(), json!(non_body_params));
244 }
245
246 if let Some(body) = body_param {
248 let request_body = convert_body_to_request_body(body, &consumes);
249 converted.insert("requestBody".to_string(), request_body);
250 } else if !form_data_params.is_empty() {
251 let mut properties = Map::new();
253 let mut required = Vec::new();
254 let mut has_file = false;
255 for param in &form_data_params {
256 if let Some(name) = param.get("name").and_then(|v| v.as_str()) {
257 let mut prop = param.as_object().map(schema_fields).unwrap_or_default();
258 has_file |= param.get("type").and_then(Value::as_str) == Some("file");
259 if let Some(desc) = param.get("description") {
260 prop.insert("description".to_string(), desc.clone());
261 }
262 properties.insert(name.to_string(), json!(prop));
263 if param.get("required").and_then(|v| v.as_bool()).unwrap_or(false) {
264 required.push(json!(name));
265 }
266 }
267 }
268 let body_required = !required.is_empty();
269 let content_type = if has_file {
270 "multipart/form-data"
271 } else {
272 "application/x-www-form-urlencoded"
273 };
274 let mut schema = Map::new();
275 schema.insert("type".to_string(), json!("object"));
276 schema.insert("properties".to_string(), json!(properties));
277 if !required.is_empty() {
278 schema.insert("required".to_string(), json!(required));
279 }
280 converted.insert(
281 "requestBody".to_string(),
282 json!({
283 "required": body_required,
284 "content": {
285 content_type: {
286 "schema": schema
287 }
288 }
289 }),
290 );
291 }
292
293 if let Some(responses) = operation.get("responses") {
295 let converted_responses = convert_responses(responses, &produces);
296 converted.insert("responses".to_string(), converted_responses);
297 }
298
299 for (key, value) in operation {
301 match key.as_str() {
302 "parameters" | "responses" | "consumes" | "produces" => {
303 }
305 _ => {
306 converted.insert(key.clone(), value.clone());
307 }
308 }
309 }
310
311 converted
312}
313
314fn convert_parameter(param: &Value) -> Value {
316 let Some(param_obj) = param.as_object() else {
317 return param.clone();
318 };
319
320 if param_obj.contains_key("$ref") {
321 return param.clone();
322 }
323 let mut converted = Map::new();
324
325 for key in &["name", "in", "description", "required", "allowEmptyValue"] {
327 if let Some(value) = param_obj.get(*key) {
328 converted.insert(key.to_string(), value.clone());
329 }
330 }
331
332 let param_in = param_obj.get("in").and_then(|v| v.as_str());
334
335 if param_in == Some("body") || param_in == Some("formData") {
337 return param.clone();
338 }
339
340 let schema = schema_fields(param_obj);
341 if !schema.is_empty() {
342 converted.insert("schema".to_string(), Value::Object(schema));
343 }
344 copy_extensions(param_obj, &mut converted);
345
346 Value::Object(converted)
347}
348
349fn convert_body_to_request_body(body: &Value, consumes: &[String]) -> Value {
351 let mut request_body = Map::new();
352
353 if let Some(desc) = body.get("description") {
354 request_body.insert("description".to_string(), desc.clone());
355 }
356
357 if let Some(required) = body.get("required") {
358 request_body.insert("required".to_string(), required.clone());
359 }
360
361 let mut content = Map::new();
363 let schema = body.get("schema").cloned().unwrap_or(json!({}));
364
365 for media_type in consumes {
366 content.insert(
367 media_type.clone(),
368 json!({
369 "schema": convert_schema_refs(&schema)
370 }),
371 );
372 }
373
374 request_body.insert("content".to_string(), Value::Object(content));
375
376 Value::Object(request_body)
377}
378
379fn convert_responses(responses: &Value, produces: &[String]) -> Value {
381 let Some(responses_obj) = responses.as_object() else {
382 return responses.clone();
383 };
384
385 let mut converted = Map::new();
386
387 for (status_code, response) in responses_obj {
388 if let Some(response_obj) = response.as_object() {
389 let converted_response = convert_response(response_obj, produces);
390 converted.insert(status_code.clone(), Value::Object(converted_response));
391 }
392 }
393
394 Value::Object(converted)
395}
396
397fn convert_response(response: &Map<String, Value>, produces: &[String]) -> Map<String, Value> {
399 if response.contains_key("$ref") {
400 return response.clone();
401 }
402 let mut converted = Map::new();
403
404 if let Some(desc) = response.get("description") {
406 converted.insert("description".to_string(), desc.clone());
407 } else {
408 converted.insert("description".to_string(), json!("Response"));
409 }
410
411 let mut content = Map::new();
413 for media_type in produces {
414 let mut media = Map::new();
415 if let Some(schema) = response.get("schema") {
416 media.insert("schema".into(), convert_schema_refs(schema));
417 }
418 if let Some(example) =
419 response.get("examples").and_then(|examples| examples.get(media_type))
420 {
421 media.insert("example".into(), example.clone());
422 }
423 if !media.is_empty() {
424 content.insert(media_type.clone(), Value::Object(media));
425 }
426 }
427 if !content.is_empty() {
428 converted.insert("content".into(), Value::Object(content));
429 }
430
431 if let Some(headers) = response.get("headers") {
433 if let Some(headers_obj) = headers.as_object() {
434 let mut converted_headers = Map::new();
435 for (name, header) in headers_obj {
436 converted_headers.insert(name.clone(), convert_header(header));
437 }
438 converted.insert("headers".to_string(), Value::Object(converted_headers));
439 }
440 }
441
442 for (name, value) in response.iter().filter(|(name, _)| name.starts_with("x-")) {
443 converted.insert(name.clone(), value.clone());
444 }
445
446 converted
447}
448
449fn convert_header(header: &Value) -> Value {
451 let Some(header_obj) = header.as_object() else {
452 return header.clone();
453 };
454
455 let mut converted = Map::new();
456
457 if let Some(desc) = header_obj.get("description") {
458 converted.insert("description".to_string(), desc.clone());
459 }
460
461 let schema = schema_fields(header_obj);
462 if !schema.is_empty() {
463 converted.insert("schema".to_string(), Value::Object(schema));
464 }
465 copy_extensions(header_obj, &mut converted);
466
467 Value::Object(converted)
468}
469
470fn convert_security_definitions(security_defs: &Value) -> Value {
472 let Some(defs_obj) = security_defs.as_object() else {
473 return security_defs.clone();
474 };
475
476 let mut converted = Map::new();
477
478 for (name, def) in defs_obj {
479 if let Some(def_obj) = def.as_object() {
480 let converted_def = convert_security_definition(def_obj);
481 converted.insert(name.clone(), Value::Object(converted_def));
482 }
483 }
484
485 Value::Object(converted)
486}
487
488fn convert_security_definition(def: &Map<String, Value>) -> Map<String, Value> {
490 let mut converted = Map::new();
491
492 let security_type = def.get("type").and_then(|v| v.as_str()).unwrap_or("");
493
494 match security_type {
495 "basic" => {
496 converted.insert("type".to_string(), json!("http"));
497 converted.insert("scheme".to_string(), json!("basic"));
498 }
499 "apiKey" => {
500 converted.insert("type".to_string(), json!("apiKey"));
501 if let Some(name) = def.get("name") {
502 converted.insert("name".to_string(), name.clone());
503 }
504 if let Some(in_val) = def.get("in") {
505 converted.insert("in".to_string(), in_val.clone());
506 }
507 }
508 "oauth2" => {
509 converted.insert("type".to_string(), json!("oauth2"));
510
511 let flow = def.get("flow").and_then(|v| v.as_str()).unwrap_or("implicit");
513 let mut flows = Map::new();
514
515 let mut flow_obj = Map::new();
516
517 let flow_name = match flow {
519 "implicit" => "implicit",
520 "password" => "password",
521 "application" => "clientCredentials",
522 "accessCode" => "authorizationCode",
523 _ => "implicit",
524 };
525
526 if let Some(auth_url) = def.get("authorizationUrl") {
527 flow_obj.insert("authorizationUrl".to_string(), auth_url.clone());
528 }
529 if let Some(token_url) = def.get("tokenUrl") {
530 flow_obj.insert("tokenUrl".to_string(), token_url.clone());
531 }
532 if let Some(scopes) = def.get("scopes") {
533 flow_obj.insert("scopes".to_string(), scopes.clone());
534 } else {
535 flow_obj.insert("scopes".to_string(), json!({}));
536 }
537
538 flows.insert(flow_name.to_string(), Value::Object(flow_obj));
539 converted.insert("flows".to_string(), Value::Object(flows));
540 }
541 _ => {
542 for (key, value) in def {
544 converted.insert(key.clone(), value.clone());
545 }
546 }
547 }
548
549 if let Some(desc) = def.get("description") {
551 converted.insert("description".to_string(), desc.clone());
552 }
553
554 if let Some(description) = def.get("description") {
555 converted.insert("description".into(), description.clone());
556 }
557 copy_extensions(def, &mut converted);
558
559 converted
560}
561
562fn copy_extensions(source: &Map<String, Value>, destination: &mut Map<String, Value>) {
563 destination.extend(
564 source
565 .iter()
566 .filter(|(name, _)| name.starts_with("x-"))
567 .map(|(name, value)| (name.clone(), value.clone())),
568 );
569}
570
571fn schema_fields(parameter: &Map<String, Value>) -> Map<String, Value> {
572 let mut schema = Map::new();
573 for name in [
574 "type",
575 "format",
576 "items",
577 "enum",
578 "default",
579 "minimum",
580 "maximum",
581 "exclusiveMinimum",
582 "exclusiveMaximum",
583 "minLength",
584 "maxLength",
585 "minItems",
586 "maxItems",
587 "uniqueItems",
588 "multipleOf",
589 "pattern",
590 ] {
591 if let Some(value) = parameter.get(name) {
592 schema.insert(name.into(), value.clone());
593 }
594 }
595 copy_extensions(parameter, &mut schema);
596 convert_schema_refs(&Value::Object(schema))
597 .as_object()
598 .cloned()
599 .unwrap_or_default()
600}
601
602fn convert_ref(ref_str: &str) -> String {
604 if let Some(name) = ref_str.strip_prefix("#/definitions/") {
606 format!("#/components/schemas/{}", name)
607 } else {
608 ref_str.to_string()
609 }
610}
611
612fn convert_schema_refs(schema: &Value) -> Value {
614 let Some(object) = schema.as_object() else {
615 return schema.clone();
616 };
617 let mut converted = object.clone();
618 for (key, value) in object {
619 let value = match key.as_str() {
620 "$ref" => value
621 .as_str()
622 .map(|reference| json!(convert_ref(reference)))
623 .unwrap_or_else(|| value.clone()),
624 "properties" | "definitions" | "patternProperties" => value
625 .as_object()
626 .map(|properties| {
627 Value::Object(
628 properties
629 .iter()
630 .map(|(name, schema)| (name.clone(), convert_schema_refs(schema)))
631 .collect(),
632 )
633 })
634 .unwrap_or_else(|| value.clone()),
635 "items" | "additionalProperties" | "additionalItems" | "not" => {
636 convert_schema_refs(value)
637 }
638 "allOf" | "anyOf" | "oneOf" => value
639 .as_array()
640 .map(|schemas| Value::Array(schemas.iter().map(convert_schema_refs).collect()))
641 .unwrap_or_else(|| value.clone()),
642 _ => value.clone(),
643 };
644 converted.insert(key.clone(), value);
645 }
646 if converted.get("type").and_then(Value::as_str) == Some("file") {
647 converted.insert("type".into(), json!("string"));
648 converted.insert("format".into(), json!("binary"));
649 }
650 Value::Object(converted)
651}
652
653pub fn is_swagger_2(value: &Value) -> bool {
655 value.get("swagger").and_then(|v| v.as_str()) == Some("2.0")
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_is_swagger_2() {
664 assert!(is_swagger_2(&json!({"swagger": "2.0"})));
665 assert!(!is_swagger_2(&json!({"openapi": "3.0.0"})));
666 }
667
668 #[test]
669 fn test_convert_servers() {
670 let swagger = json!({
671 "swagger": "2.0",
672 "host": "api.example.com",
673 "basePath": "/v1",
674 "schemes": ["https", "http"]
675 });
676
677 let servers = convert_servers(&swagger);
678 assert_eq!(servers.len(), 2);
679 assert_eq!(servers[0]["url"], "https://api.example.com/v1");
680 assert_eq!(servers[1]["url"], "http://api.example.com/v1");
681 }
682
683 #[test]
684 fn test_convert_ref() {
685 assert_eq!(convert_ref("#/definitions/User"), "#/components/schemas/User");
686 assert_eq!(convert_ref("#/components/schemas/User"), "#/components/schemas/User");
687 }
688
689 #[test]
690 fn test_convert_parameter() {
691 let param = json!({
692 "name": "userId",
693 "in": "path",
694 "required": true,
695 "type": "string",
696 "format": "uuid"
697 });
698
699 let converted = convert_parameter(¶m);
700 assert_eq!(converted["name"], "userId");
701 assert_eq!(converted["in"], "path");
702 assert_eq!(converted["schema"]["type"], "string");
703 assert_eq!(converted["schema"]["format"], "uuid");
704 }
705
706 #[test]
707 fn test_convert_security_basic() {
708 let def = json!({
709 "type": "basic",
710 "description": "Basic auth"
711 });
712
713 if let Some(def_obj) = def.as_object() {
714 let converted = convert_security_definition(def_obj);
715 assert_eq!(converted["type"], json!("http"));
716 assert_eq!(converted["scheme"], json!("basic"));
717 }
718 }
719
720 #[test]
721 fn test_basic_conversion() {
722 let swagger = json!({
723 "swagger": "2.0",
724 "info": {
725 "title": "Test API",
726 "version": "1.0.0"
727 },
728 "host": "api.example.com",
729 "basePath": "/v1",
730 "schemes": ["https"],
731 "paths": {
732 "/users": {
733 "get": {
734 "operationId": "getUsers",
735 "produces": ["application/json"],
736 "responses": {
737 "200": {
738 "description": "Success"
739 }
740 }
741 }
742 }
743 }
744 });
745
746 let result = convert_swagger_to_openapi3(&swagger).unwrap();
747 assert_eq!(result["openapi"], "3.0.3");
748 assert_eq!(result["info"]["title"], "Test API");
749 assert!(result["servers"].as_array().is_some());
750 assert!(result["paths"]["/users"]["get"].is_object());
751 }
752}