use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct StdlibApiMapping {
pub module: &'static str,
pub class: &'static str,
pub python_attr: &'static str,
pub rust_pattern: RustPattern,
}
#[derive(Debug, Clone)]
pub enum RustPattern {
MethodCall {
method: &'static str,
extra_args: Vec<&'static str>,
propagate_error: bool,
},
PropertyToMethod {
method: &'static str,
propagate_error: bool,
},
IterationPattern {
iter_method: &'static str,
element_type: Option<&'static str>,
yields_results: bool,
},
CustomTemplate { template: &'static str },
}
pub trait StdlibPlugin {
fn register_mappings(&self, registry: &mut StdlibMappings);
fn name(&self) -> &str;
fn version(&self) -> &str {
"0.1.0"
}
}
pub struct StdlibMappings {
mappings: HashMap<(String, String, String), RustPattern>,
}
impl StdlibMappings {
pub fn new() -> Self {
let mut mappings = HashMap::new();
Self::register_csv_mappings(&mut mappings);
Self::register_file_mappings(&mut mappings);
Self { mappings }
}
fn register_csv_mappings(mappings: &mut HashMap<(String, String, String), RustPattern>) {
mappings.insert(
(
"csv".to_string(),
"DictReader".to_string(),
"fieldnames".to_string(),
),
RustPattern::PropertyToMethod {
method: "headers",
propagate_error: true,
},
);
mappings.insert(
(
"csv".to_string(),
"DictReader".to_string(),
"__iter__".to_string(),
),
RustPattern::IterationPattern {
iter_method: "deserialize",
element_type: Some("HashMap<String, String>"),
yields_results: true,
},
);
mappings.insert(
(
"csv".to_string(),
"Reader".to_string(),
"fieldnames".to_string(),
),
RustPattern::PropertyToMethod {
method: "headers",
propagate_error: true,
},
);
}
fn register_file_mappings(mappings: &mut HashMap<(String, String, String), RustPattern>) {
mappings.insert(
(
"builtins".to_string(),
"file".to_string(),
"__iter__".to_string(),
),
RustPattern::CustomTemplate {
template: "BufReader::new({var}).lines()",
},
);
mappings.insert(
(
"io".to_string(),
"TextIOWrapper".to_string(),
"__iter__".to_string(),
),
RustPattern::CustomTemplate {
template: "BufReader::new({var}).lines()",
},
);
}
pub fn lookup(&self, module: &str, class: &str, attribute: &str) -> Option<&RustPattern> {
self.mappings
.get(&(module.to_string(), class.to_string(), attribute.to_string()))
}
pub fn has_iteration_mapping(&self, module: &str, class: &str) -> bool {
self.lookup(module, class, "__iter__").is_some()
}
pub fn get_iteration_pattern(&self, module: &str, class: &str) -> Option<&RustPattern> {
self.lookup(module, class, "__iter__")
}
pub fn register(&mut self, mapping: StdlibApiMapping) {
let key = (
mapping.module.to_string(),
mapping.class.to_string(),
mapping.python_attr.to_string(),
);
self.mappings.insert(key, mapping.rust_pattern);
}
pub fn register_batch(&mut self, mappings: Vec<StdlibApiMapping>) {
for mapping in mappings {
self.register(mapping);
}
}
pub fn load_plugin(&mut self, plugin: &dyn StdlibPlugin) {
plugin.register_mappings(self);
}
pub fn load_plugins(&mut self, plugins: &[&dyn StdlibPlugin]) {
for plugin in plugins {
self.load_plugin(*plugin);
}
}
}
impl Default for StdlibMappings {
fn default() -> Self {
Self::new()
}
}
impl RustPattern {
pub fn generate_rust_code(&self, base_expr: &str, original_args: &[String]) -> String {
match self {
RustPattern::MethodCall {
method,
extra_args,
propagate_error,
} => {
let mut all_args = original_args.to_vec();
all_args.extend(extra_args.iter().map(|s| s.to_string()));
let args_str = all_args.join(", ");
let call = if args_str.is_empty() {
format!("{}.{}()", base_expr, method)
} else {
format!("{}.{}({})", base_expr, method, args_str)
};
if *propagate_error {
format!("{}?", call)
} else {
call
}
}
RustPattern::PropertyToMethod {
method,
propagate_error,
} => {
let call = format!("{}.{}()", base_expr, method);
if *propagate_error {
format!("{}?", call)
} else {
call
}
}
RustPattern::IterationPattern {
iter_method,
element_type,
yields_results: _,
} => {
if let Some(elem_type) = element_type {
format!("{}.{}::<{}>()", base_expr, iter_method, elem_type)
} else {
format!("{}.{}()", base_expr, iter_method)
}
}
RustPattern::CustomTemplate { template } => template.replace("{var}", base_expr),
}
}
pub fn yields_results(&self) -> bool {
matches!(
self,
RustPattern::IterationPattern {
yields_results: true,
..
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_csv_fieldnames_mapping() {
let mappings = StdlibMappings::new();
let pattern = mappings.lookup("csv", "DictReader", "fieldnames");
assert!(pattern.is_some());
let rust_code = pattern.unwrap().generate_rust_code("reader", &[]);
assert_eq!(rust_code, "reader.headers()?");
}
#[test]
fn test_csv_iteration_mapping() {
let mappings = StdlibMappings::new();
let pattern = mappings.get_iteration_pattern("csv", "DictReader");
assert!(pattern.is_some());
let rust_code = pattern.unwrap().generate_rust_code("reader", &[]);
assert_eq!(rust_code, "reader.deserialize::<HashMap<String, String>>()");
}
#[test]
fn test_file_iteration_mapping() {
let mappings = StdlibMappings::new();
let pattern = mappings.lookup("builtins", "file", "__iter__");
assert!(pattern.is_some());
let rust_code = pattern.unwrap().generate_rust_code("f", &[]);
assert_eq!(rust_code, "BufReader::new(f).lines()");
}
#[test]
fn test_register_custom_mapping() {
let mut mappings = StdlibMappings::new();
mappings.register(StdlibApiMapping {
module: "requests",
class: "Session",
python_attr: "get",
rust_pattern: RustPattern::MethodCall {
method: "get",
extra_args: vec![],
propagate_error: true,
},
});
let pattern = mappings.lookup("requests", "Session", "get");
assert!(pattern.is_some());
let rust_code = pattern.unwrap().generate_rust_code("session", &[]);
assert_eq!(rust_code, "session.get()?");
}
#[test]
fn test_register_batch() {
let mut mappings = StdlibMappings::new();
let batch = vec![
StdlibApiMapping {
module: "numpy",
class: "ndarray",
python_attr: "shape",
rust_pattern: RustPattern::PropertyToMethod {
method: "shape",
propagate_error: false,
},
},
StdlibApiMapping {
module: "numpy",
class: "ndarray",
python_attr: "dtype",
rust_pattern: RustPattern::PropertyToMethod {
method: "dtype",
propagate_error: false,
},
},
];
mappings.register_batch(batch);
assert!(mappings.lookup("numpy", "ndarray", "shape").is_some());
assert!(mappings.lookup("numpy", "ndarray", "dtype").is_some());
}
struct TestRequestsPlugin;
impl StdlibPlugin for TestRequestsPlugin {
fn register_mappings(&self, registry: &mut StdlibMappings) {
registry.register(StdlibApiMapping {
module: "requests",
class: "Session",
python_attr: "get",
rust_pattern: RustPattern::MethodCall {
method: "get",
extra_args: vec![],
propagate_error: true,
},
});
registry.register(StdlibApiMapping {
module: "requests",
class: "Session",
python_attr: "post",
rust_pattern: RustPattern::MethodCall {
method: "post",
extra_args: vec![],
propagate_error: true,
},
});
}
fn name(&self) -> &str {
"requests"
}
fn version(&self) -> &str {
"1.0.0"
}
}
#[test]
fn test_load_plugin() {
let mut mappings = StdlibMappings::new();
let plugin = TestRequestsPlugin;
mappings.load_plugin(&plugin);
assert!(mappings.lookup("requests", "Session", "get").is_some());
assert!(mappings.lookup("requests", "Session", "post").is_some());
let get_pattern = mappings.lookup("requests", "Session", "get").unwrap();
assert_eq!(
get_pattern.generate_rust_code("session", &[]),
"session.get()?"
);
}
struct TestNumpyPlugin;
impl StdlibPlugin for TestNumpyPlugin {
fn register_mappings(&self, registry: &mut StdlibMappings) {
registry.register(StdlibApiMapping {
module: "numpy",
class: "ndarray",
python_attr: "reshape",
rust_pattern: RustPattern::MethodCall {
method: "reshape",
extra_args: vec![],
propagate_error: false,
},
});
}
fn name(&self) -> &str {
"numpy"
}
}
#[test]
fn test_load_multiple_plugins() {
let mut mappings = StdlibMappings::new();
let requests_plugin = TestRequestsPlugin;
let numpy_plugin = TestNumpyPlugin;
mappings.load_plugins(&[&requests_plugin, &numpy_plugin]);
assert!(mappings.lookup("requests", "Session", "get").is_some());
assert!(mappings.lookup("numpy", "ndarray", "reshape").is_some());
}
#[test]
fn test_plugin_override_builtin() {
let mut mappings = StdlibMappings::new();
assert!(mappings.lookup("csv", "DictReader", "fieldnames").is_some());
struct OverridePlugin;
impl StdlibPlugin for OverridePlugin {
fn register_mappings(&self, registry: &mut StdlibMappings) {
registry.register(StdlibApiMapping {
module: "csv",
class: "DictReader",
python_attr: "fieldnames",
rust_pattern: RustPattern::PropertyToMethod {
method: "get_headers", propagate_error: true,
},
});
}
fn name(&self) -> &str {
"csv_override"
}
}
mappings.load_plugin(&OverridePlugin);
let pattern = mappings.lookup("csv", "DictReader", "fieldnames").unwrap();
let code = pattern.generate_rust_code("reader", &[]);
assert_eq!(code, "reader.get_headers()?");
}
#[test]
fn test_stdlib_mappings_default() {
let mappings = StdlibMappings::default();
assert!(mappings.lookup("csv", "DictReader", "fieldnames").is_some());
assert!(mappings.lookup("csv", "DictReader", "__iter__").is_some());
}
#[test]
fn test_stdlib_api_mapping_clone() {
let mapping = StdlibApiMapping {
module: "csv",
class: "Reader",
python_attr: "test",
rust_pattern: RustPattern::PropertyToMethod {
method: "test",
propagate_error: false,
},
};
let cloned = mapping.clone();
assert_eq!(cloned.module, "csv");
assert_eq!(cloned.class, "Reader");
}
#[test]
fn test_stdlib_api_mapping_debug() {
let mapping = StdlibApiMapping {
module: "csv",
class: "Reader",
python_attr: "test",
rust_pattern: RustPattern::PropertyToMethod {
method: "test",
propagate_error: false,
},
};
let debug = format!("{:?}", mapping);
assert!(debug.contains("csv"));
assert!(debug.contains("Reader"));
}
#[test]
fn test_rust_pattern_debug() {
let pattern = RustPattern::MethodCall {
method: "test",
extra_args: vec![],
propagate_error: false,
};
let debug = format!("{:?}", pattern);
assert!(debug.contains("MethodCall"));
}
#[test]
fn test_rust_pattern_clone() {
let pattern = RustPattern::CustomTemplate {
template: "test({var})",
};
let cloned = pattern.clone();
if let RustPattern::CustomTemplate { template } = cloned {
assert_eq!(template, "test({var})");
} else {
panic!("Clone should preserve variant");
}
}
#[test]
fn test_method_call_with_args() {
let pattern = RustPattern::MethodCall {
method: "fetch",
extra_args: vec![],
propagate_error: false,
};
let code = pattern.generate_rust_code("client", &["url".to_string()]);
assert_eq!(code, "client.fetch(url)");
}
#[test]
fn test_method_call_with_extra_args() {
let pattern = RustPattern::MethodCall {
method: "fetch",
extra_args: vec!["timeout"],
propagate_error: false,
};
let code = pattern.generate_rust_code("client", &["url".to_string()]);
assert_eq!(code, "client.fetch(url, timeout)");
}
#[test]
fn test_method_call_no_propagate_error() {
let pattern = RustPattern::MethodCall {
method: "get",
extra_args: vec![],
propagate_error: false,
};
let code = pattern.generate_rust_code("obj", &[]);
assert_eq!(code, "obj.get()");
}
#[test]
fn test_method_call_propagate_error() {
let pattern = RustPattern::MethodCall {
method: "get",
extra_args: vec![],
propagate_error: true,
};
let code = pattern.generate_rust_code("obj", &[]);
assert_eq!(code, "obj.get()?");
}
#[test]
fn test_property_to_method_no_error() {
let pattern = RustPattern::PropertyToMethod {
method: "len",
propagate_error: false,
};
let code = pattern.generate_rust_code("list", &[]);
assert_eq!(code, "list.len()");
}
#[test]
fn test_property_to_method_with_error() {
let pattern = RustPattern::PropertyToMethod {
method: "headers",
propagate_error: true,
};
let code = pattern.generate_rust_code("reader", &[]);
assert_eq!(code, "reader.headers()?");
}
#[test]
fn test_iteration_pattern_no_element_type() {
let pattern = RustPattern::IterationPattern {
iter_method: "iter",
element_type: None,
yields_results: false,
};
let code = pattern.generate_rust_code("collection", &[]);
assert_eq!(code, "collection.iter()");
}
#[test]
fn test_iteration_pattern_with_element_type() {
let pattern = RustPattern::IterationPattern {
iter_method: "deserialize",
element_type: Some("Record"),
yields_results: true,
};
let code = pattern.generate_rust_code("reader", &[]);
assert_eq!(code, "reader.deserialize::<Record>()");
}
#[test]
fn test_custom_template_with_var() {
let pattern = RustPattern::CustomTemplate {
template: "Box::new({var})",
};
let code = pattern.generate_rust_code("value", &[]);
assert_eq!(code, "Box::new(value)");
}
#[test]
fn test_custom_template_multiple_vars() {
let pattern = RustPattern::CustomTemplate {
template: "process({var}).map(|x| x + {var})",
};
let code = pattern.generate_rust_code("n", &[]);
assert_eq!(code, "process(n).map(|x| x + n)");
}
#[test]
fn test_yields_results_true() {
let pattern = RustPattern::IterationPattern {
iter_method: "deserialize",
element_type: Some("Row"),
yields_results: true,
};
assert!(pattern.yields_results());
}
#[test]
fn test_yields_results_false() {
let pattern = RustPattern::IterationPattern {
iter_method: "iter",
element_type: None,
yields_results: false,
};
assert!(!pattern.yields_results());
}
#[test]
fn test_yields_results_method_call() {
let pattern = RustPattern::MethodCall {
method: "test",
extra_args: vec![],
propagate_error: true,
};
assert!(!pattern.yields_results());
}
#[test]
fn test_yields_results_property_to_method() {
let pattern = RustPattern::PropertyToMethod {
method: "test",
propagate_error: true,
};
assert!(!pattern.yields_results());
}
#[test]
fn test_yields_results_custom_template() {
let pattern = RustPattern::CustomTemplate {
template: "{var}.iter()",
};
assert!(!pattern.yields_results());
}
#[test]
fn test_has_iteration_mapping_true() {
let mappings = StdlibMappings::new();
assert!(mappings.has_iteration_mapping("csv", "DictReader"));
}
#[test]
fn test_has_iteration_mapping_false() {
let mappings = StdlibMappings::new();
assert!(!mappings.has_iteration_mapping("unknown", "Unknown"));
}
#[test]
fn test_lookup_nonexistent() {
let mappings = StdlibMappings::new();
assert!(mappings.lookup("nonexistent", "Foo", "bar").is_none());
}
#[test]
fn test_csv_reader_fieldnames() {
let mappings = StdlibMappings::new();
let pattern = mappings.lookup("csv", "Reader", "fieldnames");
assert!(pattern.is_some());
let code = pattern.unwrap().generate_rust_code("reader", &[]);
assert_eq!(code, "reader.headers()?");
}
#[test]
fn test_io_text_wrapper_iteration() {
let mappings = StdlibMappings::new();
let pattern = mappings.lookup("io", "TextIOWrapper", "__iter__");
assert!(pattern.is_some());
let code = pattern.unwrap().generate_rust_code("file", &[]);
assert_eq!(code, "BufReader::new(file).lines()");
}
#[test]
fn test_plugin_default_version() {
struct MinimalPlugin;
impl StdlibPlugin for MinimalPlugin {
fn register_mappings(&self, _registry: &mut StdlibMappings) {}
fn name(&self) -> &str {
"minimal"
}
}
let plugin = MinimalPlugin;
assert_eq!(plugin.version(), "0.1.0");
assert_eq!(plugin.name(), "minimal");
}
#[test]
fn test_plugin_custom_version() {
assert_eq!(TestRequestsPlugin.version(), "1.0.0");
}
#[test]
fn test_get_iteration_pattern_nonexistent() {
let mappings = StdlibMappings::new();
assert!(mappings
.get_iteration_pattern("unknown", "Unknown")
.is_none());
}
#[test]
fn test_method_call_with_multiple_extra_args() {
let pattern = RustPattern::MethodCall {
method: "request",
extra_args: vec!["headers", "timeout"],
propagate_error: true,
};
let code = pattern.generate_rust_code("client", &["url".to_string()]);
assert_eq!(code, "client.request(url, headers, timeout)?");
}
#[test]
fn test_empty_module_lookup() {
let mappings = StdlibMappings::new();
assert!(mappings.lookup("", "DictReader", "fieldnames").is_none());
}
#[test]
fn test_empty_class_lookup() {
let mappings = StdlibMappings::new();
assert!(mappings.lookup("csv", "", "fieldnames").is_none());
}
#[test]
fn test_empty_attribute_lookup() {
let mappings = StdlibMappings::new();
assert!(mappings.lookup("csv", "DictReader", "").is_none());
}
}