Skip to main content

portalis_transpiler/
py_to_rust_http.rs

1//! Python to Rust HTTP API Mapping
2//!
3//! Maps Python HTTP libraries to WASI-compatible Rust fetch API:
4//! - requests -> wasi_fetch
5//! - urllib.request -> wasi_fetch
6//! - httpx -> wasi_fetch
7//! - aiohttp -> wasi_fetch (async)
8//!
9//! This module provides translation patterns for common HTTP operations.
10
11use std::collections::HashMap;
12
13/// HTTP library translation patterns
14pub struct HttpMapper;
15
16impl HttpMapper {
17    /// Map Python requests.get() to Rust
18    ///
19    /// Python:
20    /// ```python
21    /// response = requests.get("https://api.example.com/data")
22    /// data = response.json()
23    /// ```
24    ///
25    /// Rust:
26    /// ```rust,no_run
27    /// let response = WasiFetch::get("https://api.example.com/data").await?;
28    /// let data: serde_json::Value = response.json()?;
29    /// ```
30    pub fn translate_requests_get(url: &str, params: Option<HashMap<String, String>>) -> String {
31        let url_with_params = if let Some(params) = params {
32            format!("{{
33    let mut query = QueryParams::new();
34    {}
35    query.append_to_url(\"{}\")
36}}",
37                params.iter()
38                    .map(|(k, v)| format!("    query.add(\"{}\", \"{}\");", k, v))
39                    .collect::<Vec<_>>()
40                    .join("\n"),
41                url
42            )
43        } else {
44            format!("\"{}\"", url)
45        };
46
47        format!("let response = WasiFetch::get({}).await?;", url_with_params)
48    }
49
50    /// Map Python requests.post() with JSON to Rust
51    ///
52    /// Python:
53    /// ```python
54    /// response = requests.post("https://api.example.com/users", json={"name": "Alice"})
55    /// ```
56    ///
57    /// Rust:
58    /// ```rust,no_run
59    /// let data = serde_json::json!({"name": "Alice"});
60    /// let response = WasiFetch::post_json("https://api.example.com/users", &data).await?;
61    /// ```
62    pub fn translate_requests_post_json(url: &str, json_var: &str) -> String {
63        format!("let response = WasiFetch::post_json(\"{}\", &{}).await?;", url, json_var)
64    }
65
66    /// Map Python requests.post() with form data to Rust
67    ///
68    /// Python:
69    /// ```python
70    /// response = requests.post("https://api.example.com/login", data={"user": "alice", "pass": "secret"})
71    /// ```
72    ///
73    /// Rust:
74    /// ```rust,no_run
75    /// let mut form = HashMap::new();
76    /// form.insert("user".to_string(), "alice".to_string());
77    /// form.insert("pass".to_string(), "secret".to_string());
78    /// let response = WasiFetch::post_form("https://api.example.com/login", form).await?;
79    /// ```
80    pub fn translate_requests_post_form(url: &str, data: HashMap<String, String>) -> String {
81        let mut code = String::new();
82        code.push_str("let mut form = HashMap::new();\n");
83
84        for (key, value) in data {
85            code.push_str(&format!("form.insert(\"{}\".to_string(), \"{}\".to_string());\n", key, value));
86        }
87
88        code.push_str(&format!("let response = WasiFetch::post_form(\"{}\", form).await?;", url));
89        code
90    }
91
92    /// Map Python requests with headers to Rust
93    ///
94    /// Python:
95    /// ```python
96    /// headers = {"Authorization": "Bearer token123"}
97    /// response = requests.get("https://api.example.com/protected", headers=headers)
98    /// ```
99    ///
100    /// Rust:
101    /// ```rust,no_run
102    /// let mut request = Request::new(Method::Get, "https://api.example.com/protected");
103    /// request.header("Authorization", "Bearer token123");
104    /// let response = WasiFetch::fetch(request).await?;
105    /// ```
106    pub fn translate_requests_with_headers(
107        method: &str,
108        url: &str,
109        headers: HashMap<String, String>
110    ) -> String {
111        let rust_method = method.to_uppercase();
112        let method_enum = match rust_method.as_str() {
113            "GET" => "Method::Get",
114            "POST" => "Method::Post",
115            "PUT" => "Method::Put",
116            "DELETE" => "Method::Delete",
117            "PATCH" => "Method::Patch",
118            _ => "Method::Get",
119        };
120
121        let mut code = format!("let mut request = Request::new({}, \"{}\");\n", method_enum, url);
122
123        for (key, value) in headers {
124            code.push_str(&format!("request.header(\"{}\", \"{}\");\n", key, value));
125        }
126
127        code.push_str("let response = WasiFetch::fetch(request).await?;");
128        code
129    }
130
131    /// Map Python urllib.request.urlopen() to Rust
132    ///
133    /// Python:
134    /// ```python
135    /// from urllib.request import urlopen
136    /// response = urlopen("https://api.example.com/data")
137    /// data = response.read()
138    /// ```
139    ///
140    /// Rust:
141    /// ```rust,no_run
142    /// let response = WasiFetch::get("https://api.example.com/data").await?;
143    /// let data = response.bytes();
144    /// ```
145    pub fn translate_urllib_urlopen(url: &str) -> String {
146        format!(
147            "let response = WasiFetch::get(\"{}\").await?;\nlet data = response.bytes();",
148            url
149        )
150    }
151
152    /// Map Python httpx async requests to Rust
153    ///
154    /// Python:
155    /// ```python
156    /// async with httpx.AsyncClient() as client:
157    ///     response = await client.get("https://api.example.com/data")
158    ///     data = response.json()
159    /// ```
160    ///
161    /// Rust:
162    /// ```rust,no_run
163    /// let response = WasiFetch::get("https://api.example.com/data").await?;
164    /// let data: serde_json::Value = response.json()?;
165    /// ```
166    pub fn translate_httpx_async_get(url: &str) -> String {
167        format!("let response = WasiFetch::get(\"{}\").await?;", url)
168    }
169
170    /// Generate complete Rust function from Python requests code
171    ///
172    /// Python:
173    /// ```python
174    /// def fetch_user(user_id):
175    ///     response = requests.get(f"https://api.example.com/users/{user_id}")
176    ///     return response.json()
177    /// ```
178    ///
179    /// Rust:
180    /// ```rust,no_run
181    /// async fn fetch_user(user_id: i32) -> Result<serde_json::Value> {
182    ///     let url = format!("https://api.example.com/users/{}", user_id);
183    ///     let response = WasiFetch::get(&url).await?;
184    ///     let data = response.json()?;
185    ///     Ok(data)
186    /// }
187    /// ```
188    pub fn generate_async_http_function(
189        func_name: &str,
190        params: Vec<(&str, &str)>,
191        return_type: &str,
192        http_call: &str,
193    ) -> String {
194        let param_str = params.iter()
195            .map(|(name, typ)| format!("{}: {}", name, typ))
196            .collect::<Vec<_>>()
197            .join(", ");
198
199        format!(
200            "async fn {}({}) -> Result<{}> {{\n    {}\n    Ok(data)\n}}",
201            func_name, param_str, return_type, http_call
202        )
203    }
204
205    /// Map response methods from Python to Rust
206    pub fn translate_response_method(method: &str) -> Option<&'static str> {
207        match method {
208            // requests library methods
209            "json()" => Some("response.json()?"),
210            ".json()" => Some("response.json()?"),
211            "text" => Some("response.text()?"),
212            ".text" => Some("response.text()?"),
213            "content" => Some("response.bytes()"),
214            ".content" => Some("response.bytes()"),
215            "status_code" => Some("response.status()"),
216            ".status_code" => Some("response.status()"),
217            "headers" => Some("response.headers()"),
218            ".headers" => Some("response.headers()"),
219
220            // urllib methods
221            "read()" => Some("response.bytes()"),
222            ".read()" => Some("response.bytes()"),
223            "getcode()" => Some("response.status()"),
224            ".getcode()" => Some("response.status()"),
225
226            _ => None,
227        }
228    }
229
230    /// Get required imports for HTTP functionality
231    pub fn get_http_imports() -> Vec<&'static str> {
232        vec![
233            "use crate::wasi_fetch::{WasiFetch, Request, Response, Method, QueryParams};",
234            "use std::collections::HashMap;",
235            "use anyhow::Result;",
236        ]
237    }
238
239    /// Get required Cargo dependencies for HTTP functionality
240    pub fn get_http_dependencies() -> Vec<(&'static str, &'static str)> {
241        vec![
242            ("reqwest", "0.11"),
243            ("tokio", "1.35"),
244            ("serde_json", "1.0"),
245        ]
246    }
247}
248
249/// Python HTTP pattern detection
250pub struct HttpPatternDetector;
251
252impl HttpPatternDetector {
253    /// Detect if Python code uses requests library
254    pub fn uses_requests(python_code: &str) -> bool {
255        python_code.contains("import requests")
256            || python_code.contains("from requests import")
257            || python_code.contains("requests.get")
258            || python_code.contains("requests.post")
259    }
260
261    /// Detect if Python code uses urllib
262    pub fn uses_urllib(python_code: &str) -> bool {
263        python_code.contains("import urllib")
264            || python_code.contains("from urllib")
265            || python_code.contains("urlopen")
266    }
267
268    /// Detect if Python code uses httpx
269    pub fn uses_httpx(python_code: &str) -> bool {
270        python_code.contains("import httpx")
271            || python_code.contains("from httpx import")
272            || python_code.contains("httpx.get")
273            || python_code.contains("httpx.AsyncClient")
274    }
275
276    /// Detect if Python code uses aiohttp
277    pub fn uses_aiohttp(python_code: &str) -> bool {
278        python_code.contains("import aiohttp")
279            || python_code.contains("from aiohttp import")
280            || python_code.contains("aiohttp.ClientSession")
281    }
282
283    /// Detect if async HTTP calls are used
284    pub fn uses_async_http(python_code: &str) -> bool {
285        (Self::uses_httpx(python_code) && python_code.contains("async"))
286            || Self::uses_aiohttp(python_code)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_translate_requests_get() {
296        let result = HttpMapper::translate_requests_get("https://api.example.com/data", None);
297        assert!(result.contains("WasiFetch::get"));
298        assert!(result.contains("https://api.example.com/data"));
299    }
300
301    #[test]
302    fn test_translate_requests_get_with_params() {
303        let mut params = HashMap::new();
304        params.insert("page".to_string(), "1".to_string());
305        params.insert("limit".to_string(), "10".to_string());
306
307        let result = HttpMapper::translate_requests_get("https://api.example.com/data", Some(params));
308        assert!(result.contains("QueryParams"));
309        assert!(result.contains("query.add"));
310    }
311
312    #[test]
313    fn test_translate_requests_post_json() {
314        let result = HttpMapper::translate_requests_post_json("https://api.example.com/users", "user_data");
315        assert!(result.contains("WasiFetch::post_json"));
316        assert!(result.contains("user_data"));
317    }
318
319    #[test]
320    fn test_translate_requests_with_headers() {
321        let mut headers = HashMap::new();
322        headers.insert("Authorization".to_string(), "Bearer token123".to_string());
323
324        let result = HttpMapper::translate_requests_with_headers("GET", "https://api.example.com", headers);
325        assert!(result.contains("Request::new"));
326        assert!(result.contains("Method::Get"));
327        assert!(result.contains("Authorization"));
328    }
329
330    #[test]
331    fn test_response_method_translation() {
332        assert_eq!(HttpMapper::translate_response_method("json()"), Some("response.json()?"));
333        assert_eq!(HttpMapper::translate_response_method("text"), Some("response.text()?"));
334        assert_eq!(HttpMapper::translate_response_method("status_code"), Some("response.status()"));
335    }
336
337    #[test]
338    fn test_pattern_detection() {
339        assert!(HttpPatternDetector::uses_requests("import requests\nrequests.get('url')"));
340        assert!(HttpPatternDetector::uses_urllib("from urllib.request import urlopen"));
341        assert!(HttpPatternDetector::uses_httpx("import httpx\nawait httpx.get('url')"));
342        assert!(HttpPatternDetector::uses_async_http("import httpx\nasync def foo():\n    await httpx.get('url')"));
343    }
344
345    #[test]
346    fn test_get_imports() {
347        let imports = HttpMapper::get_http_imports();
348        assert!(!imports.is_empty());
349        assert!(imports[0].contains("wasi_fetch"));
350    }
351
352    #[test]
353    fn test_generate_async_function() {
354        let result = HttpMapper::generate_async_http_function(
355            "fetch_user",
356            vec![("user_id", "i32")],
357            "serde_json::Value",
358            "let response = WasiFetch::get(&url).await?;\n    let data = response.json()?;"
359        );
360
361        assert!(result.contains("async fn fetch_user"));
362        assert!(result.contains("user_id: i32"));
363        assert!(result.contains("Result<serde_json::Value>"));
364    }
365}