Skip to main content

depyler_tooling/library_mapping/
toml_plugin.rs

1//! DEPYLER-0903: TOML Plugin Loader for Enterprise Library Mappings
2//!
3//! Loads library mappings from TOML configuration files.
4//!
5//! # File Format
6//!
7//! ```toml
8//! [plugin]
9//! id = "my-plugin"
10//! version = "1.0.0"
11//!
12//! [[mappings]]
13//! python_module = "my_lib"
14//! rust_crate = "my_lib_rs"
15//! python_version_req = ">=3.8"
16//! rust_crate_version = "1.0"
17//!
18//! [mappings.items]
19//! my_func = { rust_name = "my_func", pattern = "Direct" }
20//! ```
21
22use super::{
23    ItemMapping, LibraryMapping, MappingConfidence, MappingPlugin, MappingRegistry, ParamType,
24    TransformPattern, TypeTransform, ValidationError,
25};
26use serde::Deserialize;
27use std::collections::HashMap;
28use std::path::Path;
29
30/// TOML plugin configuration file structure
31#[derive(Debug, Deserialize)]
32pub struct TomlPluginConfig {
33    /// Plugin metadata
34    pub plugin: PluginMetadata,
35    /// Library mappings
36    #[serde(default)]
37    pub mappings: Vec<TomlLibraryMapping>,
38}
39
40/// Plugin metadata from TOML
41#[derive(Debug, Deserialize)]
42pub struct PluginMetadata {
43    /// Plugin identifier
44    pub id: String,
45    /// Plugin version
46    pub version: String,
47    /// Optional maintainer contact
48    #[serde(default)]
49    pub maintainer: Option<String>,
50}
51
52/// Library mapping from TOML
53#[derive(Debug, Deserialize)]
54pub struct TomlLibraryMapping {
55    /// Python module path
56    pub python_module: String,
57    /// Rust crate path
58    pub rust_crate: String,
59    /// Python version requirement
60    #[serde(default = "default_version_req")]
61    pub python_version_req: String,
62    /// Rust crate version
63    #[serde(default = "default_version_req")]
64    pub rust_crate_version: String,
65    /// Item mappings
66    #[serde(default)]
67    pub items: HashMap<String, TomlItemMapping>,
68    /// Cargo features
69    #[serde(default)]
70    pub features: Vec<String>,
71    /// Confidence level
72    #[serde(default)]
73    pub confidence: TomlConfidence,
74    /// Source documentation
75    #[serde(default)]
76    pub provenance: String,
77}
78
79fn default_version_req() -> String {
80    "*".to_string()
81}
82
83/// Item mapping from TOML
84#[derive(Debug, Deserialize)]
85pub struct TomlItemMapping {
86    /// Rust name
87    pub rust_name: String,
88    /// Transform pattern type
89    #[serde(default)]
90    pub pattern: TomlPattern,
91    /// Extra args for MethodCall
92    #[serde(default)]
93    pub extra_args: Vec<String>,
94    /// Method name for Constructor
95    #[serde(default)]
96    pub method: Option<String>,
97    /// Indices for ReorderArgs
98    #[serde(default)]
99    pub indices: Vec<usize>,
100    /// Pattern string for TypedTemplate
101    #[serde(default)]
102    pub pattern_str: Option<String>,
103    /// Params for TypedTemplate
104    #[serde(default)]
105    pub params: Vec<String>,
106    /// Param types for TypedTemplate
107    #[serde(default)]
108    pub param_types: Vec<String>,
109    /// Type transform
110    #[serde(default)]
111    pub type_transform: Option<TomlTypeTransform>,
112}
113
114/// Transform pattern name from TOML
115#[derive(Debug, Deserialize, Default)]
116#[serde(rename_all = "PascalCase")]
117pub enum TomlPattern {
118    #[default]
119    Direct,
120    MethodCall,
121    PropertyToMethod,
122    Constructor,
123    ReorderArgs,
124    TypedTemplate,
125    Template,
126}
127
128/// Type transform from TOML
129#[derive(Debug, Deserialize)]
130pub struct TomlTypeTransform {
131    pub python_type: String,
132    pub rust_type: String,
133}
134
135/// Confidence level from TOML
136#[derive(Debug, Deserialize, Default)]
137#[serde(rename_all = "PascalCase")]
138pub enum TomlConfidence {
139    Verified,
140    Community,
141    #[default]
142    Experimental,
143}
144
145/// TOML-based plugin implementation
146pub struct TomlPlugin {
147    config: TomlPluginConfig,
148}
149
150impl TomlPlugin {
151    /// Load plugin from TOML string
152    pub fn parse(toml_content: &str) -> Result<Self, TomlParseError> {
153        let config: TomlPluginConfig =
154            toml::from_str(toml_content).map_err(|e| TomlParseError {
155                message: e.to_string(),
156                // toml 0.8+ includes line info in Display output
157                line: None,
158            })?;
159        Ok(Self { config })
160    }
161
162    /// Load plugin from file path
163    pub fn from_file(path: &Path) -> Result<Self, TomlParseError> {
164        let content = std::fs::read_to_string(path).map_err(|e| TomlParseError {
165            message: format!("Failed to read file: {}", e),
166            line: None,
167        })?;
168        Self::parse(&content)
169    }
170
171    /// Convert TOML mapping to LibraryMapping
172    fn convert_mapping(toml_mapping: &TomlLibraryMapping) -> LibraryMapping {
173        let items = toml_mapping
174            .items
175            .iter()
176            .map(|(name, item)| (name.clone(), Self::convert_item(item)))
177            .collect();
178
179        LibraryMapping {
180            python_module: toml_mapping.python_module.clone(),
181            rust_crate: toml_mapping.rust_crate.clone(),
182            python_version_req: toml_mapping.python_version_req.clone(),
183            rust_crate_version: toml_mapping.rust_crate_version.clone(),
184            items,
185            features: toml_mapping.features.clone(),
186            confidence: Self::convert_confidence(&toml_mapping.confidence),
187            provenance: toml_mapping.provenance.clone(),
188        }
189    }
190
191    /// Convert TOML item mapping
192    fn convert_item(toml_item: &TomlItemMapping) -> ItemMapping {
193        let pattern = match &toml_item.pattern {
194            TomlPattern::Direct => TransformPattern::Direct,
195            TomlPattern::MethodCall => TransformPattern::MethodCall {
196                extra_args: toml_item.extra_args.clone(),
197            },
198            TomlPattern::PropertyToMethod => TransformPattern::PropertyToMethod,
199            TomlPattern::Constructor => TransformPattern::Constructor {
200                method: toml_item
201                    .method
202                    .clone()
203                    .unwrap_or_else(|| "new".to_string()),
204            },
205            TomlPattern::ReorderArgs => TransformPattern::ReorderArgs {
206                indices: toml_item.indices.clone(),
207            },
208            TomlPattern::TypedTemplate => TransformPattern::TypedTemplate {
209                pattern: toml_item.pattern_str.clone().unwrap_or_default(),
210                params: toml_item.params.clone(),
211                param_types: toml_item
212                    .param_types
213                    .iter()
214                    .map(|s| Self::parse_param_type(s))
215                    .collect(),
216            },
217            #[allow(deprecated)]
218            TomlPattern::Template => TransformPattern::Template {
219                template: toml_item.pattern_str.clone().unwrap_or_default(),
220            },
221        };
222
223        let type_transform = toml_item.type_transform.as_ref().map(|tt| TypeTransform {
224            python_type: tt.python_type.clone(),
225            rust_type: tt.rust_type.clone(),
226        });
227
228        ItemMapping {
229            rust_name: toml_item.rust_name.clone(),
230            pattern,
231            type_transform,
232        }
233    }
234
235    /// Parse ParamType from string
236    fn parse_param_type(s: &str) -> ParamType {
237        match s.to_lowercase().as_str() {
238            "expr" => ParamType::Expr,
239            "string" => ParamType::String,
240            "number" => ParamType::Number,
241            "bytes" => ParamType::Bytes,
242            "bool" => ParamType::Bool,
243            "path" => ParamType::Path,
244            "list" => ParamType::List,
245            "dict" => ParamType::Dict,
246            _ => ParamType::Expr, // Default to Expr
247        }
248    }
249
250    /// Convert confidence level
251    fn convert_confidence(conf: &TomlConfidence) -> MappingConfidence {
252        match conf {
253            TomlConfidence::Verified => MappingConfidence::Verified,
254            TomlConfidence::Community => MappingConfidence::Community,
255            TomlConfidence::Experimental => MappingConfidence::Experimental,
256        }
257    }
258}
259
260impl MappingPlugin for TomlPlugin {
261    fn id(&self) -> &str {
262        &self.config.plugin.id
263    }
264
265    fn version(&self) -> &str {
266        &self.config.plugin.version
267    }
268
269    fn register(&self, registry: &mut MappingRegistry) {
270        for toml_mapping in &self.config.mappings {
271            let mapping = Self::convert_mapping(toml_mapping);
272            registry.register_extension(mapping);
273        }
274    }
275
276    fn validate(&self) -> Result<(), ValidationError> {
277        for mapping in &self.config.mappings {
278            for (name, item) in &mapping.items {
279                // Validate ReorderArgs
280                if let TomlPattern::ReorderArgs = item.pattern {
281                    TransformPattern::validate_reorder_args(&item.indices).map_err(|mut e| {
282                        e.mapping = Some(format!("{}::{}", mapping.python_module, name));
283                        e
284                    })?;
285                }
286
287                // Validate TypedTemplate
288                if let TomlPattern::TypedTemplate = item.pattern {
289                    let pattern_str = item.pattern_str.as_deref().unwrap_or("");
290                    let param_types: Vec<_> = item
291                        .param_types
292                        .iter()
293                        .map(|s| Self::parse_param_type(s))
294                        .collect();
295                    TransformPattern::validate_typed_template(
296                        pattern_str,
297                        &item.params,
298                        &param_types,
299                    )
300                    .map_err(|mut e| {
301                        e.mapping = Some(format!("{}::{}", mapping.python_module, name));
302                        e
303                    })?;
304                }
305            }
306        }
307        Ok(())
308    }
309}
310
311/// Error parsing TOML plugin
312#[derive(Debug)]
313pub struct TomlParseError {
314    pub message: String,
315    pub line: Option<usize>,
316}
317
318impl std::fmt::Display for TomlParseError {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        if let Some(line) = self.line {
321            write!(f, "TOML parse error at line {}: {}", line, self.message)
322        } else {
323            write!(f, "TOML parse error: {}", self.message)
324        }
325    }
326}
327
328impl std::error::Error for TomlParseError {}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    // ============ parse_param_type tests ============
335
336    #[test]
337    fn test_parse_param_type_expr() {
338        assert!(matches!(
339            TomlPlugin::parse_param_type("expr"),
340            ParamType::Expr
341        ));
342        assert!(matches!(
343            TomlPlugin::parse_param_type("Expr"),
344            ParamType::Expr
345        ));
346        assert!(matches!(
347            TomlPlugin::parse_param_type("EXPR"),
348            ParamType::Expr
349        ));
350    }
351
352    #[test]
353    fn test_parse_param_type_string() {
354        assert!(matches!(
355            TomlPlugin::parse_param_type("string"),
356            ParamType::String
357        ));
358        assert!(matches!(
359            TomlPlugin::parse_param_type("String"),
360            ParamType::String
361        ));
362    }
363
364    #[test]
365    fn test_parse_param_type_number() {
366        assert!(matches!(
367            TomlPlugin::parse_param_type("number"),
368            ParamType::Number
369        ));
370        assert!(matches!(
371            TomlPlugin::parse_param_type("Number"),
372            ParamType::Number
373        ));
374    }
375
376    #[test]
377    fn test_parse_param_type_bytes() {
378        assert!(matches!(
379            TomlPlugin::parse_param_type("bytes"),
380            ParamType::Bytes
381        ));
382        assert!(matches!(
383            TomlPlugin::parse_param_type("Bytes"),
384            ParamType::Bytes
385        ));
386    }
387
388    #[test]
389    fn test_parse_param_type_bool() {
390        assert!(matches!(
391            TomlPlugin::parse_param_type("bool"),
392            ParamType::Bool
393        ));
394        assert!(matches!(
395            TomlPlugin::parse_param_type("Bool"),
396            ParamType::Bool
397        ));
398    }
399
400    #[test]
401    fn test_parse_param_type_path() {
402        assert!(matches!(
403            TomlPlugin::parse_param_type("path"),
404            ParamType::Path
405        ));
406        assert!(matches!(
407            TomlPlugin::parse_param_type("Path"),
408            ParamType::Path
409        ));
410    }
411
412    #[test]
413    fn test_parse_param_type_list() {
414        assert!(matches!(
415            TomlPlugin::parse_param_type("list"),
416            ParamType::List
417        ));
418        assert!(matches!(
419            TomlPlugin::parse_param_type("List"),
420            ParamType::List
421        ));
422    }
423
424    #[test]
425    fn test_parse_param_type_dict() {
426        assert!(matches!(
427            TomlPlugin::parse_param_type("dict"),
428            ParamType::Dict
429        ));
430        assert!(matches!(
431            TomlPlugin::parse_param_type("Dict"),
432            ParamType::Dict
433        ));
434    }
435
436    #[test]
437    fn test_parse_param_type_unknown_defaults_expr() {
438        assert!(matches!(
439            TomlPlugin::parse_param_type("unknown"),
440            ParamType::Expr
441        ));
442        assert!(matches!(
443            TomlPlugin::parse_param_type("custom"),
444            ParamType::Expr
445        ));
446        assert!(matches!(TomlPlugin::parse_param_type(""), ParamType::Expr));
447    }
448
449    // ============ convert_confidence tests ============
450
451    #[test]
452    fn test_convert_confidence_verified() {
453        let conf = TomlConfidence::Verified;
454        assert!(matches!(
455            TomlPlugin::convert_confidence(&conf),
456            MappingConfidence::Verified
457        ));
458    }
459
460    #[test]
461    fn test_convert_confidence_community() {
462        let conf = TomlConfidence::Community;
463        assert!(matches!(
464            TomlPlugin::convert_confidence(&conf),
465            MappingConfidence::Community
466        ));
467    }
468
469    #[test]
470    fn test_convert_confidence_experimental() {
471        let conf = TomlConfidence::Experimental;
472        assert!(matches!(
473            TomlPlugin::convert_confidence(&conf),
474            MappingConfidence::Experimental
475        ));
476    }
477
478    // ============ TomlParseError tests ============
479
480    #[test]
481    fn test_toml_parse_error_display_with_line() {
482        let err = TomlParseError {
483            message: "unexpected token".to_string(),
484            line: Some(42),
485        };
486        let display = format!("{}", err);
487        assert!(display.contains("line 42"));
488        assert!(display.contains("unexpected token"));
489    }
490
491    #[test]
492    fn test_toml_parse_error_display_without_line() {
493        let err = TomlParseError {
494            message: "invalid syntax".to_string(),
495            line: None,
496        };
497        let display = format!("{}", err);
498        assert!(!display.contains("line"));
499        assert!(display.contains("invalid syntax"));
500    }
501
502    // ============ default_version_req tests ============
503
504    #[test]
505    fn test_default_version_req() {
506        assert_eq!(default_version_req(), "*");
507    }
508
509    // ============ TomlPlugin parsing tests ============
510
511    #[test]
512    fn test_toml_plugin_basic_parsing() {
513        let toml = r#"
514[plugin]
515id = "test-plugin"
516version = "1.0.0"
517
518[[mappings]]
519python_module = "test_module"
520rust_crate = "test_crate"
521python_version_req = ">=3.8"
522rust_crate_version = "1.0"
523
524[mappings.items]
525test_func = { rust_name = "test_func", pattern = "Direct" }
526"#;
527
528        let plugin = TomlPlugin::parse(toml).unwrap();
529        assert_eq!(plugin.id(), "test-plugin");
530        assert_eq!(plugin.version(), "1.0.0");
531        assert_eq!(plugin.config.mappings.len(), 1);
532        assert_eq!(plugin.config.mappings[0].python_module, "test_module");
533    }
534
535    #[test]
536    fn test_toml_plugin_method_call() {
537        let toml = r#"
538[plugin]
539id = "test"
540version = "1.0.0"
541
542[[mappings]]
543python_module = "pandas"
544rust_crate = "polars"
545
546[mappings.items]
547head = { rust_name = "head", pattern = "MethodCall", extra_args = ["None"] }
548"#;
549
550        let plugin = TomlPlugin::parse(toml).unwrap();
551        let item = &plugin.config.mappings[0].items["head"];
552        assert_eq!(item.extra_args, vec!["None"]);
553    }
554
555    #[test]
556    fn test_toml_plugin_reorder_args() {
557        let toml = r#"
558[plugin]
559id = "test"
560version = "1.0.0"
561
562[[mappings]]
563python_module = "subprocess"
564rust_crate = "std::process"
565
566[mappings.items]
567run = { rust_name = "run", pattern = "ReorderArgs", indices = [0, 2, 1] }
568"#;
569
570        let plugin = TomlPlugin::parse(toml).unwrap();
571        assert!(plugin.validate().is_ok());
572    }
573
574    #[test]
575    fn test_toml_plugin_invalid_reorder_args() {
576        let toml = r#"
577[plugin]
578id = "test"
579version = "1.0.0"
580
581[[mappings]]
582python_module = "bad"
583rust_crate = "bad_crate"
584
585[mappings.items]
586bad_func = { rust_name = "bad_func", pattern = "ReorderArgs", indices = [0, 5, 1] }
587"#;
588
589        let plugin = TomlPlugin::parse(toml).unwrap();
590        let result = plugin.validate();
591        assert!(result.is_err());
592    }
593
594    #[test]
595    fn test_toml_plugin_typed_template() {
596        let toml = r#"
597[plugin]
598id = "aws"
599version = "1.0.0"
600
601[[mappings]]
602python_module = "boto3.s3"
603rust_crate = "aws_sdk_s3"
604
605[mappings.items]
606upload = { rust_name = "put_object", pattern = "TypedTemplate", pattern_str = "{client}.put_object({bucket}, {key})", params = ["client", "bucket", "key"], param_types = ["Expr", "String", "String"] }
607"#;
608
609        let plugin = TomlPlugin::parse(toml).unwrap();
610        assert!(plugin.validate().is_ok());
611    }
612
613    #[test]
614    fn test_toml_plugin_register() {
615        let toml = r#"
616[plugin]
617id = "test"
618version = "1.0.0"
619
620[[mappings]]
621python_module = "custom"
622rust_crate = "custom_rs"
623
624[mappings.items]
625func = { rust_name = "func", pattern = "Direct" }
626"#;
627
628        let plugin = TomlPlugin::parse(toml).unwrap();
629        let mut registry = MappingRegistry::new();
630        plugin.register(&mut registry);
631
632        let item = registry.lookup("custom", "func");
633        assert!(item.is_some());
634        assert_eq!(item.unwrap().rust_name, "func");
635    }
636
637    #[test]
638    fn test_toml_plugin_constructor_pattern() {
639        let toml = r#"
640[plugin]
641id = "test"
642version = "1.0.0"
643
644[[mappings]]
645python_module = "pathlib"
646rust_crate = "std::path"
647
648[mappings.items]
649Path = { rust_name = "PathBuf", pattern = "Constructor", method = "from" }
650"#;
651
652        let plugin = TomlPlugin::parse(toml).unwrap();
653        let item = &plugin.config.mappings[0].items["Path"];
654        assert_eq!(item.method, Some("from".to_string()));
655    }
656
657    #[test]
658    fn test_toml_plugin_property_to_method_pattern() {
659        let toml = r#"
660[plugin]
661id = "test"
662version = "1.0.0"
663
664[[mappings]]
665python_module = "os.path"
666rust_crate = "std::path"
667
668[mappings.items]
669exists = { rust_name = "exists", pattern = "PropertyToMethod" }
670"#;
671
672        let plugin = TomlPlugin::parse(toml).unwrap();
673        assert!(plugin.validate().is_ok());
674    }
675
676    #[test]
677    fn test_toml_plugin_with_features() {
678        let toml = r#"
679[plugin]
680id = "test"
681version = "1.0.0"
682
683[[mappings]]
684python_module = "crypto"
685rust_crate = "ring"
686features = ["std", "alloc"]
687
688[mappings.items]
689hash = { rust_name = "digest", pattern = "Direct" }
690"#;
691
692        let plugin = TomlPlugin::parse(toml).unwrap();
693        assert_eq!(plugin.config.mappings[0].features, vec!["std", "alloc"]);
694    }
695
696    #[test]
697    fn test_toml_plugin_with_confidence() {
698        let toml = r#"
699[plugin]
700id = "test"
701version = "1.0.0"
702
703[[mappings]]
704python_module = "verified_lib"
705rust_crate = "verified_rs"
706confidence = "Verified"
707provenance = "Official API mapping"
708
709[mappings.items]
710func = { rust_name = "func", pattern = "Direct" }
711"#;
712
713        let plugin = TomlPlugin::parse(toml).unwrap();
714        assert!(matches!(
715            plugin.config.mappings[0].confidence,
716            TomlConfidence::Verified
717        ));
718        assert_eq!(plugin.config.mappings[0].provenance, "Official API mapping");
719    }
720
721    #[test]
722    fn test_toml_plugin_with_type_transform() {
723        let toml = r#"
724[plugin]
725id = "test"
726version = "1.0.0"
727
728[[mappings]]
729python_module = "numpy"
730rust_crate = "ndarray"
731
732[mappings.items]
733array = { rust_name = "Array", pattern = "Constructor", type_transform = { python_type = "ndarray", rust_type = "Array<f64, Ix1>" } }
734"#;
735
736        let plugin = TomlPlugin::parse(toml).unwrap();
737        let type_transform = &plugin.config.mappings[0].items["array"].type_transform;
738        assert!(type_transform.is_some());
739        let tt = type_transform.as_ref().unwrap();
740        assert_eq!(tt.python_type, "ndarray");
741        assert_eq!(tt.rust_type, "Array<f64, Ix1>");
742    }
743
744    #[test]
745    fn test_toml_plugin_parse_error_invalid_toml() {
746        let invalid_toml = r#"
747[plugin
748id = "broken"
749"#;
750        let result = TomlPlugin::parse(invalid_toml);
751        assert!(result.is_err());
752    }
753
754    #[test]
755    fn test_toml_plugin_parse_error_missing_required() {
756        let missing_id = r#"
757[plugin]
758version = "1.0.0"
759"#;
760        let result = TomlPlugin::parse(missing_id);
761        assert!(result.is_err());
762    }
763
764    #[test]
765    fn test_toml_plugin_default_version_req_used() {
766        let toml = r#"
767[plugin]
768id = "test"
769version = "1.0.0"
770
771[[mappings]]
772python_module = "test"
773rust_crate = "test_rs"
774
775[mappings.items]
776func = { rust_name = "func", pattern = "Direct" }
777"#;
778
779        let plugin = TomlPlugin::parse(toml).unwrap();
780        // Should use default "*" for version requirements
781        assert_eq!(plugin.config.mappings[0].python_version_req, "*");
782        assert_eq!(plugin.config.mappings[0].rust_crate_version, "*");
783    }
784
785    #[test]
786    fn test_toml_plugin_multiple_mappings() {
787        let toml = r#"
788[plugin]
789id = "test"
790version = "1.0.0"
791
792[[mappings]]
793python_module = "module1"
794rust_crate = "crate1"
795
796[mappings.items]
797func1 = { rust_name = "func1", pattern = "Direct" }
798
799[[mappings]]
800python_module = "module2"
801rust_crate = "crate2"
802
803[mappings.items]
804func2 = { rust_name = "func2", pattern = "Direct" }
805"#;
806
807        let plugin = TomlPlugin::parse(toml).unwrap();
808        assert_eq!(plugin.config.mappings.len(), 2);
809        assert_eq!(plugin.config.mappings[0].python_module, "module1");
810        assert_eq!(plugin.config.mappings[1].python_module, "module2");
811    }
812
813    #[test]
814    fn test_toml_plugin_with_maintainer() {
815        let toml = r#"
816[plugin]
817id = "enterprise-plugin"
818version = "2.0.0"
819maintainer = "team@example.com"
820
821[[mappings]]
822python_module = "enterprise"
823rust_crate = "enterprise_rs"
824
825[mappings.items]
826func = { rust_name = "func", pattern = "Direct" }
827"#;
828
829        let plugin = TomlPlugin::parse(toml).unwrap();
830        assert_eq!(
831            plugin.config.plugin.maintainer,
832            Some("team@example.com".to_string())
833        );
834    }
835}