1use convert_case::{Case, Casing};
25use serde::{Deserialize, Serialize};
26use sha2::{Digest, Sha256};
27use std::{fs, path::Path};
28
29pub fn generate_transitive_output_path(
31 pattern: &str,
32 group_id: &str,
33 artifact_id: &str,
34 version: &str,
35 artifact_type: &str,
36) -> String {
37 let ext = match artifact_type.to_lowercase().as_str() {
38 "protobuf" => "proto",
39 "avro" => "avsc",
40 "json" => "json",
41 "openapi" => "yaml",
42 "asyncapi" => "yaml",
43 "graphql" => "graphql",
44 "xml" => "xsd",
45 "wsdl" => "wsdl",
46 _ => "txt",
47 };
48
49 expand_output_pattern(pattern, group_id, artifact_id, version, ext)
50}
51
52pub fn expand_output_pattern(
54 pattern: &str,
55 group_id: &str,
56 artifact_id: &str,
57 version: &str,
58 ext: &str,
59) -> String {
60 let mut result = pattern.to_string();
61
62 result = result.replace("{groupId}", group_id);
64 result = result.replace("{artifactId}", artifact_id);
65 result = result.replace("{version}", version);
66 result = result.replace("{ext}", ext);
67
68 let artifact_parts: Vec<&str> = artifact_id.split('.').collect();
71
72 if result.contains("{artifactId.path}") {
75 let path_version = if artifact_parts.len() > 1 {
76 artifact_parts[..artifact_parts.len() - 1].join("/")
77 } else {
78 String::new() };
80 result = result.replace("{artifactId.path}", &path_version);
81 }
82
83 if result.contains("{artifactId.fullPath}") {
86 let full_path_version = artifact_parts.join("/");
87 result = result.replace("{artifactId.fullPath}", &full_path_version);
88 }
89
90 if result.contains("{artifactId.snake_case}") {
93 let snake_case = artifact_id.replace('.', "_").to_lowercase();
94 result = result.replace("{artifactId.snake_case}", &snake_case);
95 }
96
97 if result.contains("{artifactId.kebab_case}") {
100 let kebab_case = artifact_id.replace('.', "-").to_lowercase();
101 result = result.replace("{artifactId.kebab_case}", &kebab_case);
102 }
103
104 if result.contains("{artifactId.lowercase}") {
106 result = result.replace("{artifactId.lowercase}", &artifact_id.to_lowercase());
107 }
108
109 if result.contains("{artifactId.last}") {
112 let last_part = artifact_parts.last().unwrap_or(&artifact_id);
113 result = result.replace("{artifactId.last}", last_part);
114 }
115
116 if result.contains("{artifactId.lastLowercase}") {
118 let last_part = artifact_parts.last().unwrap_or(&artifact_id).to_lowercase();
119 result = result.replace("{artifactId.lastLowercase}", &last_part);
120 }
121
122 if result.contains("{artifactId.lastSnakeCase}") {
124 let last_part = artifact_parts.last().unwrap_or(&artifact_id);
125 let snake_case_part = last_part.to_case(Case::Snake);
126 result = result.replace("{artifactId.lastSnakeCase}", &snake_case_part);
127 }
128
129 for (i, part) in artifact_parts.iter().enumerate() {
131 let placeholder = format!("{{artifactParts[{i}]}}");
132 result = result.replace(&placeholder, part);
133 }
134
135 result
136}
137
138pub fn resolve_output_path(
141 base_pattern: &str,
142 output_overrides: &std::collections::HashMap<String, Option<String>>,
143 registry: &str,
144 group_id: &str,
145 artifact_id: &str,
146 version: &str,
147 artifact_type: &str,
148) -> Option<String> {
149 let registry_key = format!("{registry}:{group_id}/{artifact_id}");
154 let group_key = format!("{group_id}/{artifact_id}");
155
156 if let Some(override_pattern) = output_overrides.get(®istry_key) {
157 override_pattern.as_ref().map(|pattern| {
158 expand_output_pattern(
159 pattern,
160 group_id,
161 artifact_id,
162 version,
163 &get_extension_for_type(artifact_type),
164 )
165 })
166 } else if let Some(override_pattern) = output_overrides.get(&group_key) {
167 override_pattern.as_ref().map(|pattern| {
168 expand_output_pattern(
169 pattern,
170 group_id,
171 artifact_id,
172 version,
173 &get_extension_for_type(artifact_type),
174 )
175 })
176 } else {
177 Some(generate_transitive_output_path(
178 base_pattern,
179 group_id,
180 artifact_id,
181 version,
182 artifact_type,
183 ))
184 }
185}
186
187fn get_extension_for_type(artifact_type: &str) -> String {
188 match artifact_type.to_lowercase().as_str() {
189 "protobuf" => "proto".to_string(),
190 "avro" => "avsc".to_string(),
191 "json" => "json".to_string(),
192 "openapi" => "yaml".to_string(),
193 "asyncapi" => "yaml".to_string(),
194 "graphql" => "graphql".to_string(),
195 "xml" => "xsd".to_string(),
196 "wsdl" => "wsdl".to_string(),
197 _ => "txt".to_string(),
198 }
199}
200
201#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
206#[serde(rename_all = "camelCase")]
207pub struct LockedDependency {
208 pub name: String,
210 pub registry: String,
212 pub resolved_version: String,
214 pub download_url: String,
216 pub sha256: String,
218 pub output_path: String,
220 pub group_id: String,
222 pub artifact_id: String,
224 pub version_spec: String,
226 #[serde(default)]
228 pub is_transitive: bool,
229}
230
231#[derive(Serialize, Deserialize, Debug)]
236#[serde(rename_all = "camelCase")]
237pub struct LockFile {
238 pub locked_dependencies: Vec<LockedDependency>,
240 pub lockfile_version: u32,
242 pub config_hash: String,
244 pub generated_at: String,
246 pub config_modified: Option<String>,
248}
249
250impl LockFile {
251 pub fn load(path: &Path) -> anyhow::Result<Self> {
262 let data = fs::read_to_string(path)?;
263 let lf: LockFile = serde_yaml::from_str(&data)?;
264 Ok(lf)
265 }
266
267 pub fn save(&self, path: &Path) -> anyhow::Result<()> {
275 let data = serde_yaml::to_string(self)?;
276 fs::write(path, data)?;
277 Ok(())
278 }
279
280 #[allow(dead_code)]
286 pub fn new(locked_dependencies: Vec<LockedDependency>, config_hash: String) -> Self {
287 Self::with_config_modified(locked_dependencies, config_hash, None)
288 }
289
290 pub fn with_config_modified(
297 locked_dependencies: Vec<LockedDependency>,
298 config_hash: String,
299 config_modified: Option<String>,
300 ) -> Self {
301 let now = chrono::Utc::now()
302 .timestamp_nanos_opt()
303 .unwrap_or(0)
304 .to_string();
305
306 Self {
307 locked_dependencies,
308 lockfile_version: 1,
309 config_hash,
310 generated_at: now,
311 config_modified,
312 }
313 }
314
315 pub fn is_compatible_with_config(&self, config_hash: &str) -> bool {
317 self.config_hash == config_hash
318 }
319
320 pub fn is_newer_than_config(&self, config_path: &Path) -> anyhow::Result<bool> {
322 if let Some(config_modified_str) = &self.config_modified {
323 if let Ok(config_modified_nanos) = config_modified_str.parse::<i64>() {
324 if let Ok(metadata) = fs::metadata(config_path) {
325 if let Ok(actual_modified) = metadata.modified() {
326 let actual_nanos = chrono::DateTime::<chrono::Utc>::from(actual_modified)
327 .timestamp_nanos_opt()
328 .unwrap_or(0);
329 return Ok(config_modified_nanos >= actual_nanos);
330 }
331 }
332 }
333 }
334 Ok(false)
336 }
337
338 #[allow(dead_code)]
340 pub fn is_up_to_date(
341 &self,
342 config_path: &Path,
343 current_config_hash: &str,
344 dependencies: &[LockedDependency],
345 ) -> anyhow::Result<bool> {
346 if !self.is_compatible_with_config(current_config_hash) {
348 return Ok(false);
349 }
350
351 if !self.is_newer_than_config(config_path)? {
353 return Ok(false);
354 }
355
356 if !self.dependencies_match(dependencies) {
358 return Ok(false);
359 }
360
361 Ok(true)
362 }
363
364 #[allow(dead_code)]
366 pub fn dependencies_match(&self, other_deps: &[LockedDependency]) -> bool {
367 if self.locked_dependencies.len() != other_deps.len() {
368 return false;
369 }
370
371 let self_map: std::collections::HashMap<&str, &LockedDependency> = self
373 .locked_dependencies
374 .iter()
375 .map(|d| (d.name.as_str(), d))
376 .collect();
377 let other_map: std::collections::HashMap<&str, &LockedDependency> =
378 other_deps.iter().map(|d| (d.name.as_str(), d)).collect();
379
380 self_map.len() == other_map.len()
382 && self_map.iter().all(|(name, dep)| {
383 other_map
384 .get(name)
385 .is_some_and(|other_dep| **dep == **other_dep)
386 })
387 }
388
389 pub fn compute_config_hash(
392 config_content: &str,
393 dependencies: &[crate::config::DependencyConfig],
394 ) -> String {
395 let mut hasher = Sha256::new();
396
397 let mut dep_specs: Vec<String> = dependencies
400 .iter()
401 .map(|d| {
402 format!(
403 "{}:{}:{}:{}:{}:{}",
404 d.name,
405 d.resolved_group_id(),
406 d.resolved_artifact_id(),
407 d.version,
408 d.registry,
409 d.output_path
410 )
411 })
412 .collect();
413 dep_specs.sort();
414
415 for spec in dep_specs {
416 hasher.update(spec.as_bytes());
417 }
418
419 if let Ok(config) = serde_yaml::from_str::<crate::config::RepoConfig>(config_content) {
422 let mut registry_specs: Vec<String> = config
424 .registries
425 .iter()
426 .map(|r| format!("{}:{}", r.name, r.url))
427 .collect();
428 registry_specs.sort();
429
430 for spec in registry_specs {
431 hasher.update(spec.as_bytes());
432 }
433
434 if let Some(ext_file) = &config.external_registries_file {
436 hasher.update(ext_file.as_bytes());
437 }
438 }
439
440 hex::encode(hasher.finalize())
441 }
442
443 pub fn get_config_modification_time(config_path: &Path) -> anyhow::Result<String> {
445 let metadata = fs::metadata(config_path)?;
446 let modified = metadata.modified()?;
447 let nanos = chrono::DateTime::<chrono::Utc>::from(modified)
448 .timestamp_nanos_opt()
449 .unwrap_or(0);
450 Ok(nanos.to_string())
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn create_test_config(dependencies: &[(&str, &str, &str, &str, &str, &str)]) -> String {
459 let mut deps = String::new();
460 for (name, group_id, artifact_id, version, registry, output_path) in dependencies {
461 deps.push_str(&format!(
462 r#"
463 - name: "{name}"
464 groupId: "{group_id}"
465 artifactId: "{artifact_id}"
466 version: "{version}"
467 registry: "{registry}"
468 outputPath: "{output_path}"
469"#
470 ));
471 }
472
473 format!(
474 r#"externalRegistriesFile: null
475registries: []
476dependencies:{deps}"#
477 )
478 }
479
480 fn create_test_locked_dependency(
481 name: &str,
482 registry: &str,
483 resolved_version: &str,
484 group_id: &str,
485 artifact_id: &str,
486 version_spec: &str,
487 ) -> LockedDependency {
488 LockedDependency {
489 name: name.to_string(),
490 registry: registry.to_string(),
491 resolved_version: resolved_version.to_string(),
492 download_url: format!(
493 "https://example.com/{group_id}/{artifact_id}/{resolved_version}"
494 ),
495 sha256: "dummy_hash".to_string(),
496 output_path: "./protos".to_string(),
497 group_id: group_id.to_string(),
498 artifact_id: artifact_id.to_string(),
499 version_spec: version_spec.to_string(),
500 is_transitive: false,
501 }
502 }
503
504 #[test]
505 fn test_config_hash_computation() {
506 let config1 = create_test_config(&[(
507 "dep1",
508 "com.example",
509 "service1",
510 "1.0.0",
511 "registry1",
512 "./protos",
513 )]);
514
515 let config2 = create_test_config(&[(
516 "dep1",
517 "com.example",
518 "service1",
519 "1.0.0",
520 "registry1",
521 "./protos",
522 )]);
523
524 let config3 = create_test_config(&[(
525 "dep1",
526 "com.example",
527 "service1",
528 "1.1.0",
529 "registry1",
530 "./protos",
531 )]);
532
533 use crate::config::DependencyConfig;
534 let deps1 = vec![DependencyConfig {
535 name: "dep1".to_string(),
536 group_id: Some("com.example".to_string()),
537 artifact_id: Some("service1".to_string()),
538 version: "1.0.0".to_string(),
539 registry: "registry1".to_string(),
540 output_path: "./protos".to_string(),
541 resolve_references: None,
542 }];
543
544 let deps3 = vec![DependencyConfig {
545 name: "dep1".to_string(),
546 group_id: Some("com.example".to_string()),
547 artifact_id: Some("service1".to_string()),
548 version: "1.1.0".to_string(),
549 registry: "registry1".to_string(),
550 output_path: "./protos".to_string(),
551 resolve_references: None,
552 }];
553
554 let hash1 = LockFile::compute_config_hash(&config1, &deps1);
555 let hash2 = LockFile::compute_config_hash(&config2, &deps1);
556 let hash3 = LockFile::compute_config_hash(&config3, &deps3);
557
558 assert_eq!(hash1, hash2, "Same config should produce same hash");
559 assert_ne!(
560 hash1, hash3,
561 "Different config should produce different hash"
562 );
563 }
564
565 #[test]
566 fn test_dependencies_match_order_independence() {
567 let dep1 = create_test_locked_dependency(
568 "dep1",
569 "reg1",
570 "1.0.0",
571 "com.example",
572 "service1",
573 "^1.0",
574 );
575 let dep2 = create_test_locked_dependency(
576 "dep2",
577 "reg1",
578 "2.0.0",
579 "com.example",
580 "service2",
581 "^2.0",
582 );
583
584 let deps_order1 = vec![dep1.clone(), dep2.clone()];
585 let deps_order2 = vec![dep2.clone(), dep1.clone()];
586
587 let lockfile = LockFile::new(deps_order1.clone(), "test_hash".to_string());
588
589 assert!(lockfile.dependencies_match(&deps_order1));
590 assert!(
591 lockfile.dependencies_match(&deps_order2),
592 "Order should not matter"
593 );
594 }
595
596 #[test]
597 fn test_dependencies_match_different_content() {
598 let dep1 = create_test_locked_dependency(
599 "dep1",
600 "reg1",
601 "1.0.0",
602 "com.example",
603 "service1",
604 "^1.0",
605 );
606 let dep2 = create_test_locked_dependency(
607 "dep2",
608 "reg1",
609 "2.0.0",
610 "com.example",
611 "service2",
612 "^2.0",
613 );
614 let dep1_modified = create_test_locked_dependency(
615 "dep1",
616 "reg1",
617 "1.1.0",
618 "com.example",
619 "service1",
620 "^1.0",
621 );
622
623 let deps1 = vec![dep1.clone(), dep2.clone()];
624 let deps2 = vec![dep1_modified, dep2.clone()];
625
626 let lockfile = LockFile::new(deps1.clone(), "test_hash".to_string());
627
628 assert!(lockfile.dependencies_match(&deps1));
629 assert!(
630 !lockfile.dependencies_match(&deps2),
631 "Different versions should not match"
632 );
633 }
634
635 #[test]
636 fn test_config_compatibility() {
637 let dep1 = create_test_locked_dependency(
638 "dep1",
639 "reg1",
640 "1.0.0",
641 "com.example",
642 "service1",
643 "^1.0",
644 );
645
646 let lockfile = LockFile::new(vec![dep1], "test_hash".to_string());
647
648 assert!(lockfile.is_compatible_with_config("test_hash"));
649 assert!(!lockfile.is_compatible_with_config("different_hash"));
650 }
651
652 #[test]
653 fn test_lockfile_serialization() {
654 let dep1 = create_test_locked_dependency(
655 "dep1",
656 "reg1",
657 "1.0.0",
658 "com.example",
659 "service1",
660 "^1.0",
661 );
662 let lockfile = LockFile::new(vec![dep1], "test_hash".to_string());
663
664 let serialized = serde_yaml::to_string(&lockfile).unwrap();
665 let deserialized: LockFile = serde_yaml::from_str(&serialized).unwrap();
666
667 assert_eq!(lockfile.config_hash, deserialized.config_hash);
668 assert_eq!(lockfile.lockfile_version, deserialized.lockfile_version);
669 assert_eq!(
670 lockfile.locked_dependencies.len(),
671 deserialized.locked_dependencies.len()
672 );
673 assert!(lockfile.dependencies_match(&deserialized.locked_dependencies));
674 }
675
676 #[test]
677 fn test_empty_dependencies() {
678 let lockfile = LockFile::new(vec![], "test_hash".to_string());
679
680 assert!(lockfile.dependencies_match(&[]));
681 assert!(
682 !lockfile.dependencies_match(&[create_test_locked_dependency(
683 "dep1",
684 "reg1",
685 "1.0.0",
686 "com.example",
687 "service1",
688 "^1.0"
689 )])
690 );
691 }
692
693 #[test]
694 fn test_missing_dependency() {
695 let dep1 = create_test_locked_dependency(
696 "dep1",
697 "reg1",
698 "1.0.0",
699 "com.example",
700 "service1",
701 "^1.0",
702 );
703 let dep2 = create_test_locked_dependency(
704 "dep2",
705 "reg1",
706 "2.0.0",
707 "com.example",
708 "service2",
709 "^2.0",
710 );
711
712 let lockfile = LockFile::new(vec![dep1.clone(), dep2.clone()], "test_hash".to_string());
713
714 assert!(!lockfile.dependencies_match(&[dep1])); assert!(!lockfile.dependencies_match(&[dep2])); }
717
718 #[test]
719 fn test_config_hash_deterministic_ordering() {
720 let deps1 = vec![
722 crate::config::DependencyConfig {
723 name: "dep_a".to_string(),
724 group_id: Some("com.example".to_string()),
725 artifact_id: Some("service_a".to_string()),
726 version: "1.0.0".to_string(),
727 registry: "registry1".to_string(),
728 output_path: "./protos".to_string(),
729 resolve_references: None,
730 },
731 crate::config::DependencyConfig {
732 name: "dep_b".to_string(),
733 group_id: Some("com.example".to_string()),
734 artifact_id: Some("service_b".to_string()),
735 version: "2.0.0".to_string(),
736 registry: "registry1".to_string(),
737 output_path: "./protos".to_string(),
738 resolve_references: None,
739 },
740 ];
741
742 let deps2 = vec![deps1[1].clone(), deps1[0].clone()]; let config_content = "test config";
745 let hash1 = LockFile::compute_config_hash(config_content, &deps1);
746 let hash2 = LockFile::compute_config_hash(config_content, &deps2);
747
748 assert_eq!(hash1, hash2, "Config hash should be order-independent");
749 }
750
751 #[test]
752 fn test_enhanced_config_hash_ignores_formatting() {
753 let deps = vec![crate::config::DependencyConfig {
755 name: "dep1".to_string(),
756 group_id: Some("com.example".to_string()),
757 artifact_id: Some("service1".to_string()),
758 version: "1.0.0".to_string(),
759 registry: "registry1".to_string(),
760 output_path: "./protos".to_string(),
761 resolve_references: None,
762 }];
763
764 let config1 = r#"
766externalRegistriesFile: null
767registries: []
768dependencies:
769 - name: dep1
770 groupId: com.example
771 artifactId: service1
772 version: "1.0.0"
773 registry: registry1
774 outputPath: ./protos
775"#;
776
777 let config2 = r#"
778externalRegistriesFile: null
779registries: []
780# This is a comment
781dependencies:
782 - name: dep1
783 groupId: com.example
784 artifactId: service1
785 version: "1.0.0"
786 registry: registry1
787 outputPath: ./protos
788# Another comment
789"#;
790
791 let hash1 = LockFile::compute_config_hash(config1, &deps);
792 let hash2 = LockFile::compute_config_hash(config2, &deps);
793
794 assert_eq!(
795 hash1, hash2,
796 "Config hash should ignore comments and formatting"
797 );
798 }
799
800 #[test]
801 fn test_with_config_modified() {
802 let dep1 = create_test_locked_dependency(
803 "dep1",
804 "reg1",
805 "1.0.0",
806 "com.example",
807 "service1",
808 "^1.0",
809 );
810 let config_modified = Some("1234567890123456789".to_string());
811
812 let lockfile = LockFile::with_config_modified(
813 vec![dep1],
814 "test_hash".to_string(),
815 config_modified.clone(),
816 );
817
818 assert_eq!(lockfile.config_modified, config_modified);
819 assert!(lockfile.generated_at.parse::<i64>().is_ok());
820 }
821
822 #[test]
823 fn test_is_newer_than_config_with_missing_data() {
824 let dep1 = create_test_locked_dependency(
825 "dep1",
826 "reg1",
827 "1.0.0",
828 "com.example",
829 "service1",
830 "^1.0",
831 );
832
833 let lockfile = LockFile::new(vec![dep1.clone()], "test_hash".to_string());
835 let result = lockfile
836 .is_newer_than_config(Path::new("nonexistent"))
837 .unwrap();
838 assert!(
839 !result,
840 "Should return false when config_modified is missing"
841 );
842
843 let mut lockfile_invalid = LockFile::new(vec![dep1], "test_hash".to_string());
845 lockfile_invalid.config_modified = Some("invalid_number".to_string());
846 let result = lockfile_invalid
847 .is_newer_than_config(Path::new("nonexistent"))
848 .unwrap();
849 assert!(
850 !result,
851 "Should return false when config_modified is invalid"
852 );
853 }
854
855 #[test]
856 fn test_lockfile_backwards_compatibility() {
857 let old_lockfile_yaml = r#"
859lockfileVersion: 1
860configHash: "test_hash"
861generatedAt: "1234567890"
862lockedDependencies:
863 - name: "dep1"
864 registry: "reg1"
865 resolvedVersion: "1.0.0"
866 downloadUrl: "https://example.com/dep1"
867 sha256: "dummy_hash"
868 outputPath: "./protos"
869 groupId: "com.example"
870 artifactId: "service1"
871 versionSpec: "^1.0"
872"#;
873
874 let lockfile: LockFile = serde_yaml::from_str(old_lockfile_yaml).unwrap();
875 assert!(lockfile.config_modified.is_none());
876 assert_eq!(lockfile.config_hash, "test_hash");
877 assert_eq!(lockfile.locked_dependencies.len(), 1);
878 }
879
880 #[test]
881 fn test_robust_dependency_matching() {
882 let dep1_v1 = create_test_locked_dependency(
883 "dep1",
884 "reg1",
885 "1.0.0",
886 "com.example",
887 "service1",
888 "^1.0",
889 );
890 let dep1_v2 = create_test_locked_dependency(
891 "dep1",
892 "reg1",
893 "1.0.1",
894 "com.example",
895 "service1",
896 "^1.0",
897 );
898 let dep2 = create_test_locked_dependency(
899 "dep2",
900 "reg1",
901 "2.0.0",
902 "com.example",
903 "service2",
904 "^2.0",
905 );
906
907 let lockfile = LockFile::new(vec![dep1_v1.clone(), dep2.clone()], "test_hash".to_string());
908
909 assert!(lockfile.dependencies_match(&[dep1_v1.clone(), dep2.clone()]));
911 assert!(lockfile.dependencies_match(&[dep2.clone(), dep1_v1.clone()])); assert!(!lockfile.dependencies_match(&[dep1_v2, dep2.clone()]));
915
916 assert!(!lockfile.dependencies_match(&[dep1_v1.clone()]));
918
919 let dep3 = create_test_locked_dependency(
921 "dep3",
922 "reg1",
923 "3.0.0",
924 "com.example",
925 "service3",
926 "^3.0",
927 );
928 assert!(!lockfile.dependencies_match(&[dep1_v1.clone(), dep2.clone(), dep3]));
929 }
930}
931
932#[cfg(test)]
933mod pattern_tests {
934 use super::*;
935
936 #[test]
937 fn test_artifact_id_path_transformations() {
938 let result = expand_output_pattern(
940 "protos/{artifactId.path}/{artifactId.lastLowercase}.{ext}",
941 "nprod",
942 "sp.frame.Frame",
943 "4.3.1",
944 "proto",
945 );
946 assert_eq!(result, "protos/sp/frame/frame.proto");
947
948 let result = expand_output_pattern(
950 "schemas/{artifactId.fullPath}.{ext}",
951 "nprod",
952 "sp.frame.Frame",
953 "4.3.1",
954 "avsc",
955 );
956 assert_eq!(result, "schemas/sp/frame/Frame.avsc");
957
958 let result = expand_output_pattern(
960 "protos/{artifactId.path}/{artifactId.lastLowercase}.{ext}",
961 "default",
962 "SimpleMessage",
963 "1.0.0",
964 "proto",
965 );
966 assert_eq!(result, "protos//simplemessage.proto"); let result = expand_output_pattern(
970 "protos/{artifactId.path}/{artifactId.lastLowercase}.{ext}",
971 "default",
972 "",
973 "1.0.0",
974 "proto",
975 );
976 assert_eq!(result, "protos//.proto");
977
978 let result = expand_output_pattern(
980 "protos/{artifactId.path}/{artifactId.lastSnakeCase}.{ext}",
981 "default",
982 "sp.frame.PingService",
983 "1.0.0",
984 "proto",
985 );
986 assert_eq!(result, "protos/sp/frame/ping_service.proto");
987
988 let result = expand_output_pattern(
990 "protos/{artifactId.lastSnakeCase}.{ext}",
991 "default",
992 "already_snake_case",
993 "1.0.0",
994 "proto",
995 );
996 assert_eq!(result, "protos/already_snake_case.proto");
997
998 let result = expand_output_pattern(
1000 "protos/{artifactId.lastSnakeCase}.{ext}",
1001 "default",
1002 "com.example.XMLHttpRequest",
1003 "1.0.0",
1004 "proto",
1005 );
1006 assert_eq!(result, "protos/xml_http_request.proto");
1007 }
1008
1009 #[test]
1010 fn test_resolve_output_path_with_null_override() {
1011 use std::collections::HashMap;
1012
1013 let mut overrides = HashMap::new();
1014 overrides.insert(
1015 "nprod/sp.frame.Frame".to_string(),
1016 Some("protos/sp/frame/frame.{ext}".to_string()),
1017 );
1018 overrides.insert("nprod/sp.internal.Debug".to_string(), None); let result = resolve_output_path(
1022 "references/{groupId}/{artifactId}.{ext}",
1023 &overrides,
1024 "nprod-apicurio",
1025 "nprod",
1026 "sp.frame.Frame",
1027 "4.3.1",
1028 "PROTOBUF",
1029 );
1030 assert_eq!(result, Some("protos/sp/frame/frame.proto".to_string()));
1031
1032 let result = resolve_output_path(
1034 "references/{groupId}/{artifactId}.{ext}",
1035 &overrides,
1036 "nprod-apicurio",
1037 "nprod",
1038 "sp.internal.Debug",
1039 "1.0.0",
1040 "PROTOBUF",
1041 );
1042 assert_eq!(result, None);
1043
1044 let result = resolve_output_path(
1046 "references/{groupId}/{artifactId}.{ext}",
1047 &overrides,
1048 "nprod-apicurio",
1049 "nprod",
1050 "sp.other.Service",
1051 "2.0.0",
1052 "PROTOBUF",
1053 );
1054 assert_eq!(
1055 result,
1056 Some("references/nprod/sp.other.Service.proto".to_string())
1057 );
1058 }
1059}