Skip to main content

portalis_transpiler/
common_libraries_translator.rs

1//! Common Python Libraries Translation
2//!
3//! Translates popular Python libraries to their Rust equivalents:
4//! - Requests → reqwest (HTTP client)
5//! - pytest → Rust testing framework
6//! - pydantic → serde with validator
7//! - logging → tracing
8//! - argparse → clap
9//! - and more
10
11use std::collections::HashMap;
12
13/// Python library to translate
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum PythonLibrary {
16    Requests,
17    Pytest,
18    Pydantic,
19    Logging,
20    Argparse,
21    Json,
22    Datetime,
23    Pathlib,
24    Regex,
25    Os,
26    Sys,
27    Collections,
28}
29
30/// Requests HTTP operations
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum RequestsOp {
33    Get,
34    Post,
35    Put,
36    Delete,
37    Patch,
38    Head,
39    Options,
40    Session,
41}
42
43/// Pytest testing operations
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum PytestOp {
46    Test,
47    Fixture,
48    Parametrize,
49    Assert,
50    Raises,
51    Skip,
52    Mark,
53}
54
55/// Pydantic validation operations
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum PydanticOp {
58    BaseModel,
59    Field,
60    Validator,
61    RootValidator,
62    Config,
63}
64
65/// Logging operations
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum LoggingOp {
68    Debug,
69    Info,
70    Warning,
71    Error,
72    Critical,
73    Logger,
74}
75
76/// Argparse operations
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ArgparseOp {
79    ArgumentParser,
80    AddArgument,
81    ParseArgs,
82}
83
84/// Common libraries translator
85pub struct CommonLibrariesTranslator {
86    /// Required imports
87    imports: HashMap<PythonLibrary, Vec<String>>,
88    /// Cargo dependencies
89    dependencies: HashMap<PythonLibrary, Vec<(&'static str, &'static str)>>,
90}
91
92impl CommonLibrariesTranslator {
93    pub fn new() -> Self {
94        let mut translator = Self {
95            imports: HashMap::new(),
96            dependencies: HashMap::new(),
97        };
98
99        translator.initialize_mappings();
100        translator
101    }
102
103    fn initialize_mappings(&mut self) {
104        // Requests → reqwest
105        self.dependencies.insert(PythonLibrary::Requests, vec![
106            ("reqwest", "0.11"),
107            ("tokio", "1.0"),
108        ]);
109
110        // Pytest → built-in testing
111        self.dependencies.insert(PythonLibrary::Pytest, vec![]);
112
113        // Pydantic → serde + validator
114        self.dependencies.insert(PythonLibrary::Pydantic, vec![
115            ("serde", "1.0"),
116            ("serde_json", "1.0"),
117            ("validator", "0.16"),
118        ]);
119
120        // Logging → tracing
121        self.dependencies.insert(PythonLibrary::Logging, vec![
122            ("tracing", "0.1"),
123            ("tracing-subscriber", "0.3"),
124        ]);
125
126        // Argparse → clap
127        self.dependencies.insert(PythonLibrary::Argparse, vec![
128            ("clap", "4.0"),
129        ]);
130
131        // Json → serde_json
132        self.dependencies.insert(PythonLibrary::Json, vec![
133            ("serde_json", "1.0"),
134        ]);
135
136        // Datetime → chrono
137        self.dependencies.insert(PythonLibrary::Datetime, vec![
138            ("chrono", "0.4"),
139        ]);
140
141        // Re → regex
142        self.dependencies.insert(PythonLibrary::Regex, vec![
143            ("regex", "1.0"),
144        ]);
145    }
146
147    /// Translate Requests HTTP operation
148    pub fn translate_requests(&mut self, op: &RequestsOp, args: &[String]) -> String {
149        self.add_import(PythonLibrary::Requests, "use reqwest");
150
151        match op {
152            RequestsOp::Get => {
153                // requests.get(url) → reqwest::get(url).await?
154                if args.is_empty() {
155                    return "reqwest::get(\"url\").await?.text().await?".to_string();
156                }
157                format!("reqwest::get({}).await?.text().await?", args[0])
158            }
159            RequestsOp::Post => {
160                // requests.post(url, json=data) → reqwest::Client::new().post(url).json(&data).send().await?
161                if args.len() >= 2 {
162                    format!("reqwest::Client::new().post({}).json(&{}).send().await?", args[0], args[1])
163                } else {
164                    "reqwest::Client::new().post(url).send().await?".to_string()
165                }
166            }
167            RequestsOp::Put => {
168                if args.is_empty() {
169                    return "reqwest::Client::new().put(url).send().await?".to_string();
170                }
171                format!("reqwest::Client::new().put({}).send().await?", args[0])
172            }
173            RequestsOp::Delete => {
174                if args.is_empty() {
175                    return "reqwest::Client::new().delete(url).send().await?".to_string();
176                }
177                format!("reqwest::Client::new().delete({}).send().await?", args[0])
178            }
179            RequestsOp::Patch => {
180                if args.is_empty() {
181                    return "reqwest::Client::new().patch(url).send().await?".to_string();
182                }
183                format!("reqwest::Client::new().patch({}).send().await?", args[0])
184            }
185            RequestsOp::Head => {
186                if args.is_empty() {
187                    return "reqwest::Client::new().head(url).send().await?".to_string();
188                }
189                format!("reqwest::Client::new().head({}).send().await?", args[0])
190            }
191            RequestsOp::Options => {
192                "reqwest::Client::new().request(Method::OPTIONS, url).send().await?".to_string()
193            }
194            RequestsOp::Session => {
195                // requests.Session() → reqwest::Client::new()
196                "reqwest::Client::new()".to_string()
197            }
198        }
199    }
200
201    /// Translate pytest test
202    pub fn translate_pytest(&mut self, op: &PytestOp, args: &[String]) -> String {
203        match op {
204            PytestOp::Test => {
205                // def test_foo(): → #[test] fn test_foo()
206                if args.is_empty() {
207                    return "#[test]\nfn test_foo() {\n    // test body\n}".to_string();
208                }
209                format!("#[test]\nfn {}() {{\n    // test body\n}}", args[0])
210            }
211            PytestOp::Fixture => {
212                // @pytest.fixture → setup function
213                "// Use setup/teardown or lazy_static for fixtures".to_string()
214            }
215            PytestOp::Parametrize => {
216                // @pytest.mark.parametrize → use test cases macro or loop
217                "#[test_case::test_case(/* cases */)]".to_string()
218            }
219            PytestOp::Assert => {
220                // assert x == y → assert_eq!(x, y)
221                if args.len() >= 2 {
222                    format!("assert_eq!({}, {});", args[0], args[1])
223                } else {
224                    "assert!(condition);".to_string()
225                }
226            }
227            PytestOp::Raises => {
228                // with pytest.raises(Exception): → #[should_panic]
229                "#[should_panic(expected = \"...\")]".to_string()
230            }
231            PytestOp::Skip => {
232                // @pytest.mark.skip → #[ignore]
233                "#[ignore]".to_string()
234            }
235            PytestOp::Mark => {
236                // @pytest.mark.slow → #[cfg_attr(...)]
237                "#[cfg_attr(not(feature = \"slow_tests\"), ignore)]".to_string()
238            }
239        }
240    }
241
242    /// Translate pydantic model
243    pub fn translate_pydantic(&mut self, op: &PydanticOp, args: &[String]) -> String {
244        self.add_import(PythonLibrary::Pydantic, "use serde::{Serialize, Deserialize}");
245        self.add_import(PythonLibrary::Pydantic, "use validator::Validate");
246
247        match op {
248            PydanticOp::BaseModel => {
249                // class Model(BaseModel): → #[derive(Serialize, Deserialize, Validate)]
250                if args.is_empty() {
251                    return "#[derive(Debug, Serialize, Deserialize, Validate)]\nstruct Model {\n    // fields\n}".to_string();
252                }
253                format!("#[derive(Debug, Serialize, Deserialize, Validate)]\nstruct {} {{\n    // fields\n}}", args[0])
254            }
255            PydanticOp::Field => {
256                // field: int = Field(gt=0) → #[validate(range(min = 0))]
257                if args.len() >= 2 {
258                    format!("#[validate({})]\npub {}: {},", args[1], args[0], "T")
259                } else {
260                    "pub field: T,".to_string()
261                }
262            }
263            PydanticOp::Validator => {
264                // @validator → custom validation function
265                "// Implement custom validation in separate function".to_string()
266            }
267            PydanticOp::RootValidator => {
268                // @root_validator → validate entire struct
269                "// Implement validation in struct impl".to_string()
270            }
271            PydanticOp::Config => {
272                // class Config: → derive attributes
273                "#[serde(rename_all = \"camelCase\")]".to_string()
274            }
275        }
276    }
277
278    /// Translate logging
279    pub fn translate_logging(&mut self, op: &LoggingOp, args: &[String]) -> String {
280        self.add_import(PythonLibrary::Logging, "use tracing::{debug, info, warn, error}");
281
282        match op {
283            LoggingOp::Debug => {
284                if args.is_empty() {
285                    return "debug!(\"message\");".to_string();
286                }
287                format!("debug!({});", args.join(", "))
288            }
289            LoggingOp::Info => {
290                if args.is_empty() {
291                    return "info!(\"message\");".to_string();
292                }
293                format!("info!({});", args.join(", "))
294            }
295            LoggingOp::Warning => {
296                if args.is_empty() {
297                    return "warn!(\"message\");".to_string();
298                }
299                format!("warn!({});", args.join(", "))
300            }
301            LoggingOp::Error => {
302                if args.is_empty() {
303                    return "error!(\"message\");".to_string();
304                }
305                format!("error!({});", args.join(", "))
306            }
307            LoggingOp::Critical => {
308                if args.is_empty() {
309                    return "error!(\"CRITICAL: message\");".to_string();
310                }
311                format!("error!(\"CRITICAL: {{}}\", {});", args.join(", "))
312            }
313            LoggingOp::Logger => {
314                // logging.getLogger(__name__) → tracing setup
315                "// Use tracing_subscriber::fmt::init()".to_string()
316            }
317        }
318    }
319
320    /// Translate argparse
321    pub fn translate_argparse(&mut self, op: &ArgparseOp, args: &[String]) -> String {
322        self.add_import(PythonLibrary::Argparse, "use clap::Parser");
323
324        match op {
325            ArgparseOp::ArgumentParser => {
326                // ArgumentParser() → #[derive(Parser)] struct
327                "#[derive(Parser, Debug)]\n#[command(author, version, about)]\nstruct Args {\n    // fields\n}".to_string()
328            }
329            ArgparseOp::AddArgument => {
330                // parser.add_argument('--name') → struct field with attribute
331                if args.is_empty() {
332                    return "#[arg(short, long)]\nfield: String,".to_string();
333                }
334                format!("#[arg({})]\n{}: String,", args[0], args.get(1).unwrap_or(&"field".to_string()))
335            }
336            ArgparseOp::ParseArgs => {
337                // parser.parse_args() → Args::parse()
338                "Args::parse()".to_string()
339            }
340        }
341    }
342
343    /// Translate datetime operations
344    pub fn translate_datetime(&self, operation: &str, args: &[String]) -> String {
345        match operation {
346            "now" => {
347                // datetime.now() → Utc::now() or Local::now()
348                "chrono::Utc::now()".to_string()
349            }
350            "date" => {
351                // datetime.date(2024, 1, 1) → NaiveDate::from_ymd_opt(2024, 1, 1)
352                if args.len() >= 3 {
353                    format!("chrono::NaiveDate::from_ymd_opt({}, {}, {}).unwrap()", args[0], args[1], args[2])
354                } else {
355                    "chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()".to_string()
356                }
357            }
358            "strftime" => {
359                // dt.strftime('%Y-%m-%d') → dt.format("%Y-%m-%d").to_string()
360                if args.is_empty() {
361                    return "dt.format(\"%Y-%m-%d\").to_string()".to_string();
362                }
363                format!("dt.format({}).to_string()", args[0])
364            }
365            "strptime" => {
366                // datetime.strptime(s, '%Y-%m-%d') → NaiveDate::parse_from_str(s, "%Y-%m-%d")
367                if args.len() >= 2 {
368                    format!("chrono::NaiveDateTime::parse_from_str({}, {}).unwrap()", args[0], args[1])
369                } else {
370                    "chrono::NaiveDateTime::parse_from_str(s, \"%Y-%m-%d\").unwrap()".to_string()
371                }
372            }
373            "timedelta" => {
374                // timedelta(days=1) → Duration::days(1)
375                "chrono::Duration::days(1)".to_string()
376            }
377            _ => format!("/* datetime.{} */", operation),
378        }
379    }
380
381    /// Translate pathlib operations
382    pub fn translate_pathlib(&self, operation: &str, args: &[String]) -> String {
383        match operation {
384            "Path" => {
385                // Path('file.txt') → Path::new("file.txt")
386                if args.is_empty() {
387                    return "std::path::Path::new(\"file.txt\")".to_string();
388                }
389                format!("std::path::Path::new({})", args[0])
390            }
391            "exists" => {
392                // path.exists() → path.exists()
393                "path.exists()".to_string()
394            }
395            "is_file" => {
396                "path.is_file()".to_string()
397            }
398            "is_dir" => {
399                "path.is_dir()".to_string()
400            }
401            "read_text" => {
402                // path.read_text() → std::fs::read_to_string(path)?
403                "std::fs::read_to_string(path)?".to_string()
404            }
405            "write_text" => {
406                // path.write_text(s) → std::fs::write(path, s)?
407                if args.is_empty() {
408                    return "std::fs::write(path, content)?".to_string();
409                }
410                format!("std::fs::write(path, {})?", args[0])
411            }
412            "mkdir" => {
413                // path.mkdir() → std::fs::create_dir(path)?
414                "std::fs::create_dir(path)?".to_string()
415            }
416            "rmdir" => {
417                "std::fs::remove_dir(path)?".to_string()
418            }
419            "glob" => {
420                // path.glob('*.txt') → use glob crate
421                if args.is_empty() {
422                    return "glob::glob(\"*.txt\")?".to_string();
423                }
424                format!("glob::glob({})?", args[0])
425            }
426            _ => format!("/* pathlib.{} */", operation),
427        }
428    }
429
430    /// Translate regex operations
431    pub fn translate_regex(&self, operation: &str, args: &[String]) -> String {
432        match operation {
433            "compile" => {
434                // re.compile(r'pattern') → Regex::new(r"pattern")?
435                if args.is_empty() {
436                    return "regex::Regex::new(r\"pattern\")?".to_string();
437                }
438                format!("regex::Regex::new({})?", args[0])
439            }
440            "match" | "search" => {
441                // re.match(pattern, text) → regex.is_match(text)
442                if args.len() >= 2 {
443                    format!("regex::Regex::new({})?.is_match({})", args[0], args[1])
444                } else {
445                    "regex.is_match(text)".to_string()
446                }
447            }
448            "findall" => {
449                // re.findall(pattern, text) → regex.find_iter(text).collect()
450                "regex.find_iter(text).map(|m| m.as_str()).collect::<Vec<_>>()".to_string()
451            }
452            "sub" => {
453                // re.sub(pattern, repl, text) → regex.replace_all(text, repl)
454                if args.len() >= 2 {
455                    format!("regex.replace_all({}, {})", args[1], args[0])
456                } else {
457                    "regex.replace_all(text, replacement)".to_string()
458                }
459            }
460            _ => format!("/* re.{} */", operation),
461        }
462    }
463
464    /// Add import for library
465    fn add_import(&mut self, lib: PythonLibrary, import: &str) {
466        self.imports.entry(lib)
467            .or_default()
468            .push(import.to_string());
469    }
470
471    /// Get imports for specific library
472    pub fn get_imports(&self, lib: &PythonLibrary) -> Vec<String> {
473        self.imports.get(lib).cloned().unwrap_or_default()
474    }
475
476    /// Get all cargo dependencies
477    pub fn get_cargo_dependencies(&self) -> Vec<(&'static str, &'static str)> {
478        let mut deps = Vec::new();
479        for lib_deps in self.dependencies.values() {
480            deps.extend(lib_deps.iter().copied());
481        }
482        deps.sort();
483        deps.dedup();
484        deps
485    }
486
487    /// Get dependencies for specific library
488    pub fn get_library_dependencies(&self, lib: &PythonLibrary) -> Vec<(&'static str, &'static str)> {
489        self.dependencies.get(lib).cloned().unwrap_or_default()
490    }
491}
492
493impl Default for CommonLibrariesTranslator {
494    fn default() -> Self {
495        Self::new()
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_requests_get() {
505        let mut translator = CommonLibrariesTranslator::new();
506        let result = translator.translate_requests(&RequestsOp::Get, &["\"https://api.example.com\"".to_string()]);
507        assert!(result.contains("reqwest::get"));
508        assert!(result.contains("await"));
509    }
510
511    #[test]
512    fn test_pytest_test() {
513        let mut translator = CommonLibrariesTranslator::new();
514        let result = translator.translate_pytest(&PytestOp::Test, &["test_example".to_string()]);
515        assert!(result.contains("#[test]"));
516        assert!(result.contains("test_example"));
517    }
518
519    #[test]
520    fn test_pydantic_model() {
521        let mut translator = CommonLibrariesTranslator::new();
522        let result = translator.translate_pydantic(&PydanticOp::BaseModel, &["User".to_string()]);
523        assert!(result.contains("Serialize"));
524        assert!(result.contains("Deserialize"));
525        assert!(result.contains("User"));
526    }
527
528    #[test]
529    fn test_logging() {
530        let mut translator = CommonLibrariesTranslator::new();
531        let result = translator.translate_logging(&LoggingOp::Info, &["\"message\"".to_string()]);
532        assert!(result.contains("info!"));
533    }
534
535    #[test]
536    fn test_datetime() {
537        let translator = CommonLibrariesTranslator::new();
538        let result = translator.translate_datetime("now", &[]);
539        assert!(result.contains("chrono"));
540    }
541}