turbomcp_openapi/
parser.rs1use std::path::Path;
4use std::time::Duration;
5
6use openapiv3::OpenAPI;
7use url::Url;
8
9use crate::error::{OpenApiError, Result};
10use crate::security::validate_url_for_ssrf;
11
12const DEFAULT_SPEC_FETCH_TIMEOUT_SECS: u64 = 30;
13
14pub fn parse_spec(content: &str) -> Result<OpenAPI> {
20 if content.trim_start().starts_with('{') {
21 match serde_json::from_str::<OpenAPI>(content) {
22 Ok(spec) => return Ok(spec),
23 Err(json_err) => {
24 if let Ok(spec) = serde_norway::from_str::<OpenAPI>(content) {
27 return Ok(spec);
28 }
29 return Err(json_err.into());
30 }
31 }
32 }
33
34 serde_norway::from_str(content).map_err(Into::into)
35}
36
37pub fn load_from_file(path: &Path) -> Result<OpenAPI> {
39 let content = std::fs::read_to_string(path)?;
40 parse_spec(&content)
41}
42
43pub async fn fetch_from_url(url: &str) -> Result<OpenAPI> {
45 let url = Url::parse(url)?;
46 validate_url_for_ssrf(&url)?;
47
48 let client = reqwest::Client::builder()
49 .timeout(Duration::from_secs(DEFAULT_SPEC_FETCH_TIMEOUT_SECS))
50 .build()?;
51 let response = client.get(url).send().await?;
52
53 if !response.status().is_success() {
54 return Err(OpenApiError::ApiError(format!(
55 "HTTP {} fetching OpenAPI spec",
56 response.status()
57 )));
58 }
59
60 let content = response.text().await?;
61 parse_spec(&content)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 const SIMPLE_SPEC_JSON: &str = r#"{
69 "openapi": "3.0.0",
70 "info": {
71 "title": "Test API",
72 "version": "1.0.0"
73 },
74 "paths": {
75 "/users": {
76 "get": {
77 "summary": "List users",
78 "responses": {
79 "200": {
80 "description": "Success"
81 }
82 }
83 }
84 }
85 }
86 }"#;
87
88 const SIMPLE_SPEC_YAML: &str = r#"
89openapi: "3.0.0"
90info:
91 title: Test API
92 version: "1.0.0"
93paths:
94 /users:
95 get:
96 summary: List users
97 responses:
98 "200":
99 description: Success
100"#;
101
102 #[test]
103 fn test_parse_json() {
104 let spec = parse_spec(SIMPLE_SPEC_JSON).unwrap();
105 assert_eq!(spec.info.title, "Test API");
106 assert!(spec.paths.paths.contains_key("/users"));
107 }
108
109 #[test]
110 fn test_parse_yaml() {
111 let spec = parse_spec(SIMPLE_SPEC_YAML).unwrap();
112 assert_eq!(spec.info.title, "Test API");
113 assert!(spec.paths.paths.contains_key("/users"));
114 }
115
116 #[test]
117 fn test_invalid_spec() {
118 let result = parse_spec("not valid openapi");
119 assert!(result.is_err());
120 }
121
122 #[tokio::test]
123 async fn test_fetch_from_url_blocks_localhost_before_request() {
124 let result = fetch_from_url("http://127.0.0.1:9/openapi.json").await;
125 assert!(matches!(result, Err(OpenApiError::SsrfBlocked(_))));
126 }
127}