1use 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#[derive(Debug, Deserialize)]
32pub struct TomlPluginConfig {
33 pub plugin: PluginMetadata,
35 #[serde(default)]
37 pub mappings: Vec<TomlLibraryMapping>,
38}
39
40#[derive(Debug, Deserialize)]
42pub struct PluginMetadata {
43 pub id: String,
45 pub version: String,
47 #[serde(default)]
49 pub maintainer: Option<String>,
50}
51
52#[derive(Debug, Deserialize)]
54pub struct TomlLibraryMapping {
55 pub python_module: String,
57 pub rust_crate: String,
59 #[serde(default = "default_version_req")]
61 pub python_version_req: String,
62 #[serde(default = "default_version_req")]
64 pub rust_crate_version: String,
65 #[serde(default)]
67 pub items: HashMap<String, TomlItemMapping>,
68 #[serde(default)]
70 pub features: Vec<String>,
71 #[serde(default)]
73 pub confidence: TomlConfidence,
74 #[serde(default)]
76 pub provenance: String,
77}
78
79fn default_version_req() -> String {
80 "*".to_string()
81}
82
83#[derive(Debug, Deserialize)]
85pub struct TomlItemMapping {
86 pub rust_name: String,
88 #[serde(default)]
90 pub pattern: TomlPattern,
91 #[serde(default)]
93 pub extra_args: Vec<String>,
94 #[serde(default)]
96 pub method: Option<String>,
97 #[serde(default)]
99 pub indices: Vec<usize>,
100 #[serde(default)]
102 pub pattern_str: Option<String>,
103 #[serde(default)]
105 pub params: Vec<String>,
106 #[serde(default)]
108 pub param_types: Vec<String>,
109 #[serde(default)]
111 pub type_transform: Option<TomlTypeTransform>,
112}
113
114#[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#[derive(Debug, Deserialize)]
130pub struct TomlTypeTransform {
131 pub python_type: String,
132 pub rust_type: String,
133}
134
135#[derive(Debug, Deserialize, Default)]
137#[serde(rename_all = "PascalCase")]
138pub enum TomlConfidence {
139 Verified,
140 Community,
141 #[default]
142 Experimental,
143}
144
145pub struct TomlPlugin {
147 config: TomlPluginConfig,
148}
149
150impl TomlPlugin {
151 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 line: None,
158 })?;
159 Ok(Self { config })
160 }
161
162 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 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 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 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, }
248 }
249
250 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 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 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 ¶m_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#[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 #[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 #[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 #[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 #[test]
505 fn test_default_version_req() {
506 assert_eq!(default_version_req(), "*");
507 }
508
509 #[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 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}