1use crate::{BUNDLE_VERSION, BundleError, BundleResult, Platform};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Manifest {
16 pub bundle_version: String,
18
19 pub plugin: PluginInfo,
21
22 pub platforms: HashMap<String, PlatformInfo>,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub build_info: Option<BuildInfo>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub sbom: Option<Sbom>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub schema_checksum: Option<String>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub notices: Option<String>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub api: Option<ApiInfo>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub public_key: Option<String>,
52
53 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
55 pub schemas: HashMap<String, SchemaInfo>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct PluginInfo {
61 pub name: String,
63
64 pub version: String,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub description: Option<String>,
70
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub authors: Vec<String>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub license: Option<String>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub repository: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PlatformInfo {
90 pub variants: HashMap<String, VariantInfo>,
93}
94
95impl PlatformInfo {
96 pub fn new(library: String, checksum: String) -> Self {
98 let mut variants = HashMap::new();
99 variants.insert(
100 "release".to_string(),
101 VariantInfo {
102 library,
103 checksum,
104 build: None,
105 },
106 );
107 Self { variants }
108 }
109
110 #[must_use]
112 pub fn release(&self) -> Option<&VariantInfo> {
113 self.variants.get("release")
114 }
115
116 #[must_use]
118 pub fn variant(&self, name: &str) -> Option<&VariantInfo> {
119 self.variants.get(name)
120 }
121
122 #[must_use]
124 pub fn default_variant(&self) -> Option<&VariantInfo> {
125 self.release()
126 }
127
128 #[must_use]
130 pub fn variant_names(&self) -> Vec<&str> {
131 self.variants.keys().map(String::as_str).collect()
132 }
133
134 #[must_use]
136 pub fn has_variant(&self, name: &str) -> bool {
137 self.variants.contains_key(name)
138 }
139
140 pub fn add_variant(&mut self, name: String, info: VariantInfo) {
142 self.variants.insert(name, info);
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct VariantInfo {
152 pub library: String,
155
156 pub checksum: String,
159
160 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub build: Option<serde_json::Value>,
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176pub struct BuildInfo {
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub built_by: Option<String>,
180
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub built_at: Option<String>,
184
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub host: Option<String>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub compiler: Option<String>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub rustbridge_version: Option<String>,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub git: Option<GitInfo>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct GitInfo {
208 pub commit: String,
210
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub branch: Option<String>,
214
215 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub tag: Option<String>,
218
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub dirty: Option<bool>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct Sbom {
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub cyclonedx: Option<String>,
233
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub spdx: Option<String>,
237}
238
239fn is_valid_variant_name(name: &str) -> bool {
244 if name.is_empty() {
245 return false;
246 }
247
248 let chars: Vec<char> = name.chars().collect();
250 if !chars[0].is_ascii_lowercase() && !chars[0].is_ascii_digit() {
251 return false;
252 }
253 if !chars[chars.len() - 1].is_ascii_lowercase() && !chars[chars.len() - 1].is_ascii_digit() {
254 return false;
255 }
256
257 name.chars()
259 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct SchemaInfo {
265 pub path: String,
267
268 pub format: String,
270
271 pub checksum: String,
273
274 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub description: Option<String>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct ApiInfo {
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub min_rustbridge_version: Option<String>,
285
286 #[serde(default)]
288 pub transports: Vec<String>,
289
290 #[serde(default)]
292 pub messages: Vec<MessageInfo>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct MessageInfo {
298 pub type_tag: String,
300
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub description: Option<String>,
304
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub request_schema: Option<String>,
308
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub response_schema: Option<String>,
312
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub message_id: Option<u32>,
316
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub cstruct_request: Option<String>,
320
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub cstruct_response: Option<String>,
324}
325
326impl Manifest {
327 #[must_use]
329 pub fn new(name: &str, version: &str) -> Self {
330 Self {
331 bundle_version: BUNDLE_VERSION.to_string(),
332 plugin: PluginInfo {
333 name: name.to_string(),
334 version: version.to_string(),
335 description: None,
336 authors: Vec::new(),
337 license: None,
338 repository: None,
339 },
340 platforms: HashMap::new(),
341 build_info: None,
342 sbom: None,
343 schema_checksum: None,
344 notices: None,
345 api: None,
346 public_key: None,
347 schemas: HashMap::new(),
348 }
349 }
350
351 pub fn add_platform(&mut self, platform: Platform, library_path: &str, checksum: &str) {
356 let platform_key = platform.as_str().to_string();
357
358 if let Some(platform_info) = self.platforms.get_mut(&platform_key) {
359 platform_info.variants.insert(
361 "release".to_string(),
362 VariantInfo {
363 library: library_path.to_string(),
364 checksum: format!("sha256:{checksum}"),
365 build: None,
366 },
367 );
368 } else {
369 self.platforms.insert(
371 platform_key,
372 PlatformInfo::new(library_path.to_string(), format!("sha256:{checksum}")),
373 );
374 }
375 }
376
377 pub fn add_platform_variant(
381 &mut self,
382 platform: Platform,
383 variant: &str,
384 library_path: &str,
385 checksum: &str,
386 build: Option<serde_json::Value>,
387 ) {
388 let platform_key = platform.as_str().to_string();
389
390 let platform_info = self
391 .platforms
392 .entry(platform_key)
393 .or_insert_with(|| PlatformInfo {
394 variants: HashMap::new(),
395 });
396
397 platform_info.variants.insert(
398 variant.to_string(),
399 VariantInfo {
400 library: library_path.to_string(),
401 checksum: format!("sha256:{checksum}"),
402 build,
403 },
404 );
405 }
406
407 pub fn set_public_key(&mut self, public_key: String) {
409 self.public_key = Some(public_key);
410 }
411
412 pub fn add_schema(
414 &mut self,
415 name: String,
416 path: String,
417 format: String,
418 checksum: String,
419 description: Option<String>,
420 ) {
421 self.schemas.insert(
422 name,
423 SchemaInfo {
424 path,
425 format,
426 checksum,
427 description,
428 },
429 );
430 }
431
432 pub fn set_build_info(&mut self, build_info: BuildInfo) {
434 self.build_info = Some(build_info);
435 }
436
437 #[must_use]
439 pub fn get_build_info(&self) -> Option<&BuildInfo> {
440 self.build_info.as_ref()
441 }
442
443 pub fn set_sbom(&mut self, sbom: Sbom) {
445 self.sbom = Some(sbom);
446 }
447
448 #[must_use]
450 pub fn get_sbom(&self) -> Option<&Sbom> {
451 self.sbom.as_ref()
452 }
453
454 pub fn set_schema_checksum(&mut self, checksum: String) {
456 self.schema_checksum = Some(checksum);
457 }
458
459 #[must_use]
461 pub fn get_schema_checksum(&self) -> Option<&str> {
462 self.schema_checksum.as_deref()
463 }
464
465 pub fn set_notices(&mut self, path: String) {
467 self.notices = Some(path);
468 }
469
470 #[must_use]
472 pub fn get_notices(&self) -> Option<&str> {
473 self.notices.as_deref()
474 }
475
476 #[must_use]
480 pub fn get_variant(&self, platform: Platform, variant: Option<&str>) -> Option<&VariantInfo> {
481 let platform_info = self.platforms.get(platform.as_str())?;
482 let variant_name = variant.unwrap_or("release");
483 platform_info.variants.get(variant_name)
484 }
485
486 #[must_use]
488 pub fn get_release_variant(&self, platform: Platform) -> Option<&VariantInfo> {
489 self.get_variant(platform, Some("release"))
490 }
491
492 #[must_use]
494 pub fn list_variants(&self, platform: Platform) -> Vec<&str> {
495 self.platforms
496 .get(platform.as_str())
497 .map(|p| p.variant_names())
498 .unwrap_or_default()
499 }
500
501 #[must_use]
503 pub fn get_platform(&self, platform: Platform) -> Option<&PlatformInfo> {
504 self.platforms.get(platform.as_str())
505 }
506
507 #[must_use]
509 pub fn supports_platform(&self, platform: Platform) -> bool {
510 self.platforms.contains_key(platform.as_str())
511 }
512
513 #[must_use]
515 pub fn supported_platforms(&self) -> Vec<Platform> {
516 self.platforms
517 .keys()
518 .filter_map(|k| Platform::parse(k))
519 .collect()
520 }
521
522 pub fn validate(&self) -> BundleResult<()> {
524 if self.bundle_version.is_empty() {
526 return Err(BundleError::InvalidManifest(
527 "bundle_version is required".to_string(),
528 ));
529 }
530
531 if self.plugin.name.is_empty() {
533 return Err(BundleError::InvalidManifest(
534 "plugin.name is required".to_string(),
535 ));
536 }
537
538 if self.plugin.version.is_empty() {
540 return Err(BundleError::InvalidManifest(
541 "plugin.version is required".to_string(),
542 ));
543 }
544
545 if self.platforms.is_empty() {
547 return Err(BundleError::InvalidManifest(
548 "at least one platform must be defined".to_string(),
549 ));
550 }
551
552 for (key, info) in &self.platforms {
554 if Platform::parse(key).is_none() {
555 return Err(BundleError::InvalidManifest(format!(
556 "unknown platform: {key}"
557 )));
558 }
559
560 if info.variants.is_empty() {
562 return Err(BundleError::InvalidManifest(format!(
563 "platform {key}: at least one variant is required"
564 )));
565 }
566
567 if !info.variants.contains_key("release") {
569 return Err(BundleError::InvalidManifest(format!(
570 "platform {key}: 'release' variant is required"
571 )));
572 }
573
574 for (variant_name, variant_info) in &info.variants {
576 if !is_valid_variant_name(variant_name) {
578 return Err(BundleError::InvalidManifest(format!(
579 "platform {key}: invalid variant name '{variant_name}' \
580 (must be lowercase alphanumeric with hyphens)"
581 )));
582 }
583
584 if variant_info.library.is_empty() {
585 return Err(BundleError::InvalidManifest(format!(
586 "platform {key}, variant {variant_name}: library path is required"
587 )));
588 }
589
590 if variant_info.checksum.is_empty() {
591 return Err(BundleError::InvalidManifest(format!(
592 "platform {key}, variant {variant_name}: checksum is required"
593 )));
594 }
595
596 if !variant_info.checksum.starts_with("sha256:") {
597 return Err(BundleError::InvalidManifest(format!(
598 "platform {key}, variant {variant_name}: checksum must start with 'sha256:'"
599 )));
600 }
601 }
602 }
603
604 Ok(())
605 }
606
607 pub fn to_json(&self) -> BundleResult<String> {
609 Ok(serde_json::to_string_pretty(self)?)
610 }
611
612 pub fn from_json(json: &str) -> BundleResult<Self> {
614 Ok(serde_json::from_str(json)?)
615 }
616}
617
618impl Default for ApiInfo {
619 fn default() -> Self {
620 Self {
621 min_rustbridge_version: None,
622 transports: vec!["json".to_string()],
623 messages: Vec::new(),
624 }
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 #![allow(non_snake_case)]
631
632 use super::*;
633
634 #[test]
635 fn Manifest___new___creates_valid_minimal_manifest() {
636 let manifest = Manifest::new("test-plugin", "1.0.0");
637
638 assert_eq!(manifest.plugin.name, "test-plugin");
639 assert_eq!(manifest.plugin.version, "1.0.0");
640 assert_eq!(manifest.bundle_version, BUNDLE_VERSION);
641 assert!(manifest.platforms.is_empty());
642 }
643
644 #[test]
645 fn Manifest___add_platform___adds_platform_info() {
646 let mut manifest = Manifest::new("test-plugin", "1.0.0");
647 manifest.add_platform(
648 Platform::LinuxX86_64,
649 "lib/linux-x86_64/libtest.so",
650 "abc123",
651 );
652
653 assert!(manifest.supports_platform(Platform::LinuxX86_64));
654 assert!(!manifest.supports_platform(Platform::WindowsX86_64));
655
656 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
657 let release = info.release().unwrap();
658 assert_eq!(release.library, "lib/linux-x86_64/libtest.so");
659 assert_eq!(release.checksum, "sha256:abc123");
660 }
661
662 #[test]
663 fn Manifest___add_platform___overwrites_existing() {
664 let mut manifest = Manifest::new("test", "1.0.0");
665 manifest.add_platform(Platform::LinuxX86_64, "lib/old.so", "old");
666 manifest.add_platform(Platform::LinuxX86_64, "lib/new.so", "new");
667
668 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
669 let release = info.release().unwrap();
670 assert_eq!(release.library, "lib/new.so");
671 assert_eq!(release.checksum, "sha256:new");
672 }
673
674 #[test]
675 fn Manifest___validate___rejects_empty_name() {
676 let manifest = Manifest::new("", "1.0.0");
677 let result = manifest.validate();
678
679 assert!(result.is_err());
680 assert!(result.unwrap_err().to_string().contains("plugin.name"));
681 }
682
683 #[test]
684 fn Manifest___validate___rejects_empty_version() {
685 let manifest = Manifest::new("test", "");
686 let result = manifest.validate();
687
688 assert!(result.is_err());
689 assert!(result.unwrap_err().to_string().contains("plugin.version"));
690 }
691
692 #[test]
693 fn Manifest___validate___rejects_empty_platforms() {
694 let manifest = Manifest::new("test", "1.0.0");
695 let result = manifest.validate();
696
697 assert!(result.is_err());
698 assert!(
699 result
700 .unwrap_err()
701 .to_string()
702 .contains("at least one platform")
703 );
704 }
705
706 #[test]
707 fn Manifest___validate___rejects_invalid_checksum_format() {
708 let mut manifest = Manifest::new("test", "1.0.0");
709 let mut variants = HashMap::new();
711 variants.insert(
712 "release".to_string(),
713 VariantInfo {
714 library: "lib/test.so".to_string(),
715 checksum: "abc123".to_string(), build: None,
717 },
718 );
719 manifest
720 .platforms
721 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
722
723 let result = manifest.validate();
724
725 assert!(result.is_err());
726 assert!(result.unwrap_err().to_string().contains("sha256:"));
727 }
728
729 #[test]
730 fn Manifest___validate___rejects_unknown_platform() {
731 let mut manifest = Manifest::new("test", "1.0.0");
732 manifest.platforms.insert(
734 "invalid-platform".to_string(),
735 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string()),
736 );
737
738 let result = manifest.validate();
739
740 assert!(result.is_err());
741 assert!(result.unwrap_err().to_string().contains("unknown platform"));
742 }
743
744 #[test]
745 fn Manifest___validate___rejects_empty_library_path() {
746 let mut manifest = Manifest::new("test", "1.0.0");
747 let mut variants = HashMap::new();
748 variants.insert(
749 "release".to_string(),
750 VariantInfo {
751 library: "".to_string(),
752 checksum: "sha256:abc123".to_string(),
753 build: None,
754 },
755 );
756 manifest
757 .platforms
758 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
759
760 let result = manifest.validate();
761
762 assert!(result.is_err());
763 assert!(
764 result
765 .unwrap_err()
766 .to_string()
767 .contains("library path is required")
768 );
769 }
770
771 #[test]
772 fn Manifest___validate___rejects_empty_checksum() {
773 let mut manifest = Manifest::new("test", "1.0.0");
774 let mut variants = HashMap::new();
775 variants.insert(
776 "release".to_string(),
777 VariantInfo {
778 library: "lib/test.so".to_string(),
779 checksum: "".to_string(),
780 build: None,
781 },
782 );
783 manifest
784 .platforms
785 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
786
787 let result = manifest.validate();
788
789 assert!(result.is_err());
790 assert!(
791 result
792 .unwrap_err()
793 .to_string()
794 .contains("checksum is required")
795 );
796 }
797
798 #[test]
799 fn Manifest___validate___rejects_missing_release_variant() {
800 let mut manifest = Manifest::new("test", "1.0.0");
801 let mut variants = HashMap::new();
802 variants.insert(
803 "debug".to_string(), VariantInfo {
805 library: "lib/test.so".to_string(),
806 checksum: "sha256:abc123".to_string(),
807 build: None,
808 },
809 );
810 manifest
811 .platforms
812 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
813
814 let result = manifest.validate();
815
816 assert!(result.is_err());
817 assert!(
818 result
819 .unwrap_err()
820 .to_string()
821 .contains("'release' variant is required")
822 );
823 }
824
825 #[test]
826 fn Manifest___validate___rejects_invalid_variant_name() {
827 let mut manifest = Manifest::new("test", "1.0.0");
828 let mut variants = HashMap::new();
829 variants.insert(
830 "release".to_string(),
831 VariantInfo {
832 library: "lib/test.so".to_string(),
833 checksum: "sha256:abc123".to_string(),
834 build: None,
835 },
836 );
837 variants.insert(
838 "INVALID".to_string(), VariantInfo {
840 library: "lib/test.so".to_string(),
841 checksum: "sha256:abc123".to_string(),
842 build: None,
843 },
844 );
845 manifest
846 .platforms
847 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
848
849 let result = manifest.validate();
850
851 assert!(result.is_err());
852 assert!(
853 result
854 .unwrap_err()
855 .to_string()
856 .contains("invalid variant name")
857 );
858 }
859
860 #[test]
861 fn Manifest___validate___accepts_valid_manifest() {
862 let mut manifest = Manifest::new("test-plugin", "1.0.0");
863 manifest.add_platform(
864 Platform::LinuxX86_64,
865 "lib/linux-x86_64/libtest.so",
866 "abc123",
867 );
868
869 assert!(manifest.validate().is_ok());
870 }
871
872 #[test]
873 fn Manifest___validate___accepts_all_platforms() {
874 let mut manifest = Manifest::new("all-platforms", "1.0.0");
875 for platform in Platform::all() {
876 manifest.add_platform(
877 *platform,
878 &format!("lib/{}/libtest", platform.as_str()),
879 "hash",
880 );
881 }
882
883 assert!(manifest.validate().is_ok());
884 assert_eq!(manifest.supported_platforms().len(), 6);
885 }
886
887 #[test]
888 fn Manifest___json_roundtrip___preserves_data() {
889 let mut manifest = Manifest::new("test-plugin", "1.0.0");
890 manifest.plugin.description = Some("A test plugin".to_string());
891 manifest.add_platform(
892 Platform::LinuxX86_64,
893 "lib/linux-x86_64/libtest.so",
894 "abc123",
895 );
896 manifest.add_platform(
897 Platform::DarwinAarch64,
898 "lib/darwin-aarch64/libtest.dylib",
899 "def456",
900 );
901
902 let json = manifest.to_json().unwrap();
903 let parsed = Manifest::from_json(&json).unwrap();
904
905 assert_eq!(parsed.plugin.name, manifest.plugin.name);
906 assert_eq!(parsed.plugin.version, manifest.plugin.version);
907 assert_eq!(parsed.plugin.description, manifest.plugin.description);
908 assert_eq!(parsed.platforms.len(), 2);
909 }
910
911 #[test]
912 fn Manifest___json_roundtrip___preserves_all_plugin_fields() {
913 let mut manifest = Manifest::new("full-plugin", "2.3.4");
914 manifest.plugin.description = Some("Full description".to_string());
915 manifest.plugin.authors = vec!["Author 1".to_string(), "Author 2".to_string()];
916 manifest.plugin.license = Some("Apache-2.0".to_string());
917 manifest.plugin.repository = Some("https://github.com/test/repo".to_string());
918 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
919
920 let json = manifest.to_json().unwrap();
921 let parsed = Manifest::from_json(&json).unwrap();
922
923 assert_eq!(parsed.plugin.description, manifest.plugin.description);
924 assert_eq!(parsed.plugin.authors, manifest.plugin.authors);
925 assert_eq!(parsed.plugin.license, manifest.plugin.license);
926 assert_eq!(parsed.plugin.repository, manifest.plugin.repository);
927 }
928
929 #[test]
930 fn Manifest___json_roundtrip___preserves_api_info() {
931 let mut manifest = Manifest::new("api-plugin", "1.0.0");
932 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
933 manifest.api = Some(ApiInfo {
934 min_rustbridge_version: Some("0.2.0".to_string()),
935 transports: vec!["json".to_string(), "binary".to_string()],
936 messages: vec![MessageInfo {
937 type_tag: "user.create".to_string(),
938 description: Some("Create a user".to_string()),
939 request_schema: Some("#/schemas/CreateUserRequest".to_string()),
940 response_schema: Some("#/schemas/CreateUserResponse".to_string()),
941 message_id: Some(1),
942 cstruct_request: None,
943 cstruct_response: None,
944 }],
945 });
946
947 let json = manifest.to_json().unwrap();
948 let parsed = Manifest::from_json(&json).unwrap();
949
950 let api = parsed.api.unwrap();
951 assert_eq!(api.min_rustbridge_version, Some("0.2.0".to_string()));
952 assert_eq!(api.transports, vec!["json", "binary"]);
953 assert_eq!(api.messages.len(), 1);
954 assert_eq!(api.messages[0].type_tag, "user.create");
955 assert_eq!(api.messages[0].message_id, Some(1));
956 }
957
958 #[test]
959 fn Manifest___json_roundtrip___preserves_schemas() {
960 let mut manifest = Manifest::new("schema-plugin", "1.0.0");
961 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
962 manifest.add_schema(
963 "messages.h".to_string(),
964 "schema/messages.h".to_string(),
965 "c-header".to_string(),
966 "sha256:abc".to_string(),
967 Some("C header for binary transport".to_string()),
968 );
969
970 let json = manifest.to_json().unwrap();
971 let parsed = Manifest::from_json(&json).unwrap();
972
973 assert_eq!(parsed.schemas.len(), 1);
974 let schema = parsed.schemas.get("messages.h").unwrap();
975 assert_eq!(schema.path, "schema/messages.h");
976 assert_eq!(schema.format, "c-header");
977 assert_eq!(schema.checksum, "sha256:abc");
978 assert_eq!(
979 schema.description,
980 Some("C header for binary transport".to_string())
981 );
982 }
983
984 #[test]
985 fn Manifest___json_roundtrip___preserves_public_key() {
986 let mut manifest = Manifest::new("signed-plugin", "1.0.0");
987 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
988 manifest.set_public_key("RWSxxxxxxxxxxxxxxxx".to_string());
989
990 let json = manifest.to_json().unwrap();
991 let parsed = Manifest::from_json(&json).unwrap();
992
993 assert_eq!(parsed.public_key, Some("RWSxxxxxxxxxxxxxxxx".to_string()));
994 }
995
996 #[test]
997 fn Manifest___from_json___invalid_json___returns_error() {
998 let result = Manifest::from_json("{ invalid }");
999
1000 assert!(result.is_err());
1001 }
1002
1003 #[test]
1004 fn Manifest___from_json___missing_required_fields___returns_error() {
1005 let result = Manifest::from_json(r#"{"bundle_version": "1.0"}"#);
1006
1007 assert!(result.is_err());
1008 }
1009
1010 #[test]
1011 fn Manifest___supported_platforms___returns_all_platforms() {
1012 let mut manifest = Manifest::new("test", "1.0.0");
1013 manifest.add_platform(Platform::LinuxX86_64, "lib/a.so", "a");
1014 manifest.add_platform(Platform::DarwinAarch64, "lib/b.dylib", "b");
1015
1016 let platforms = manifest.supported_platforms();
1017 assert_eq!(platforms.len(), 2);
1018 assert!(platforms.contains(&Platform::LinuxX86_64));
1019 assert!(platforms.contains(&Platform::DarwinAarch64));
1020 }
1021
1022 #[test]
1023 fn Manifest___get_platform___returns_none_for_unsupported() {
1024 let mut manifest = Manifest::new("test", "1.0.0");
1025 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1026
1027 assert!(manifest.get_platform(Platform::LinuxX86_64).is_some());
1028 assert!(manifest.get_platform(Platform::WindowsX86_64).is_none());
1029 }
1030
1031 #[test]
1032 fn ApiInfo___default___includes_json_transport() {
1033 let api = ApiInfo::default();
1034
1035 assert_eq!(api.transports, vec!["json"]);
1036 assert!(api.messages.is_empty());
1037 assert!(api.min_rustbridge_version.is_none());
1038 }
1039
1040 #[test]
1041 fn BuildInfo___default___all_fields_none() {
1042 let build_info = BuildInfo::default();
1043
1044 assert!(build_info.built_by.is_none());
1045 assert!(build_info.built_at.is_none());
1046 assert!(build_info.host.is_none());
1047 assert!(build_info.compiler.is_none());
1048 assert!(build_info.rustbridge_version.is_none());
1049 assert!(build_info.git.is_none());
1050 }
1051
1052 #[test]
1053 fn Manifest___build_info___roundtrip() {
1054 let mut manifest = Manifest::new("test", "1.0.0");
1055 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1056 manifest.set_build_info(BuildInfo {
1057 built_by: Some("GitHub Actions".to_string()),
1058 built_at: Some("2025-01-26T10:30:00Z".to_string()),
1059 host: Some("x86_64-unknown-linux-gnu".to_string()),
1060 compiler: Some("rustc 1.90.0".to_string()),
1061 rustbridge_version: Some("0.2.0".to_string()),
1062 git: Some(GitInfo {
1063 commit: "abc123".to_string(),
1064 branch: Some("main".to_string()),
1065 tag: Some("v1.0.0".to_string()),
1066 dirty: Some(false),
1067 }),
1068 });
1069
1070 let json = manifest.to_json().unwrap();
1071 let parsed = Manifest::from_json(&json).unwrap();
1072
1073 let build_info = parsed.get_build_info().unwrap();
1074 assert_eq!(build_info.built_by, Some("GitHub Actions".to_string()));
1075 assert_eq!(build_info.compiler, Some("rustc 1.90.0".to_string()));
1076
1077 let git = build_info.git.as_ref().unwrap();
1078 assert_eq!(git.commit, "abc123");
1079 assert_eq!(git.branch, Some("main".to_string()));
1080 assert_eq!(git.dirty, Some(false));
1081 }
1082
1083 #[test]
1084 fn Manifest___sbom___roundtrip() {
1085 let mut manifest = Manifest::new("test", "1.0.0");
1086 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1087 manifest.set_sbom(Sbom {
1088 cyclonedx: Some("sbom/sbom.cdx.json".to_string()),
1089 spdx: Some("sbom/sbom.spdx.json".to_string()),
1090 });
1091
1092 let json = manifest.to_json().unwrap();
1093 let parsed = Manifest::from_json(&json).unwrap();
1094
1095 let sbom = parsed.get_sbom().unwrap();
1096 assert_eq!(sbom.cyclonedx, Some("sbom/sbom.cdx.json".to_string()));
1097 assert_eq!(sbom.spdx, Some("sbom/sbom.spdx.json".to_string()));
1098 }
1099
1100 #[test]
1101 fn Manifest___variants___roundtrip() {
1102 let mut manifest = Manifest::new("test", "1.0.0");
1103 manifest.add_platform_variant(
1104 Platform::LinuxX86_64,
1105 "release",
1106 "lib/linux-x86_64/release/libtest.so",
1107 "hash1",
1108 Some(serde_json::json!({
1109 "profile": "release",
1110 "opt_level": "3"
1111 })),
1112 );
1113 manifest.add_platform_variant(
1114 Platform::LinuxX86_64,
1115 "debug",
1116 "lib/linux-x86_64/debug/libtest.so",
1117 "hash2",
1118 Some(serde_json::json!({
1119 "profile": "debug",
1120 "opt_level": "0"
1121 })),
1122 );
1123
1124 let json = manifest.to_json().unwrap();
1125 let parsed = Manifest::from_json(&json).unwrap();
1126
1127 let variants = parsed.list_variants(Platform::LinuxX86_64);
1128 assert_eq!(variants.len(), 2);
1129 assert!(variants.contains(&"release"));
1130 assert!(variants.contains(&"debug"));
1131
1132 let release = parsed
1133 .get_variant(Platform::LinuxX86_64, Some("release"))
1134 .unwrap();
1135 assert_eq!(release.library, "lib/linux-x86_64/release/libtest.so");
1136 assert_eq!(
1137 release.build.as_ref().unwrap()["profile"],
1138 serde_json::json!("release")
1139 );
1140 }
1141
1142 #[test]
1143 fn Manifest___schema_checksum___roundtrip() {
1144 let mut manifest = Manifest::new("test", "1.0.0");
1145 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1146 manifest.set_schema_checksum("sha256:abcdef123456".to_string());
1147
1148 let json = manifest.to_json().unwrap();
1149 let parsed = Manifest::from_json(&json).unwrap();
1150
1151 assert_eq!(parsed.get_schema_checksum(), Some("sha256:abcdef123456"));
1152 }
1153
1154 #[test]
1155 fn Manifest___notices___roundtrip() {
1156 let mut manifest = Manifest::new("test", "1.0.0");
1157 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1158 manifest.set_notices("docs/NOTICES.txt".to_string());
1159
1160 let json = manifest.to_json().unwrap();
1161 let parsed = Manifest::from_json(&json).unwrap();
1162
1163 assert_eq!(parsed.get_notices(), Some("docs/NOTICES.txt"));
1164 }
1165
1166 #[test]
1167 fn is_valid_variant_name___accepts_valid_names() {
1168 assert!(is_valid_variant_name("release"));
1169 assert!(is_valid_variant_name("debug"));
1170 assert!(is_valid_variant_name("nightly"));
1171 assert!(is_valid_variant_name("opt-size"));
1172 assert!(is_valid_variant_name("v1"));
1173 assert!(is_valid_variant_name("build123"));
1174 }
1175
1176 #[test]
1177 fn is_valid_variant_name___rejects_invalid_names() {
1178 assert!(!is_valid_variant_name("")); assert!(!is_valid_variant_name("RELEASE")); assert!(!is_valid_variant_name("Release")); assert!(!is_valid_variant_name("-debug")); assert!(!is_valid_variant_name("debug-")); assert!(!is_valid_variant_name("debug build")); assert!(!is_valid_variant_name("debug_build")); }
1186
1187 #[test]
1188 fn PlatformInfo___new___creates_release_variant() {
1189 let platform_info =
1190 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string());
1191
1192 assert!(platform_info.has_variant("release"));
1193 let release = platform_info.release().unwrap();
1194 assert_eq!(release.library, "lib/test.so");
1195 assert_eq!(release.checksum, "sha256:abc123");
1196 }
1197
1198 #[test]
1199 fn PlatformInfo___variant_names___returns_all_variants() {
1200 let mut platform_info =
1201 PlatformInfo::new("lib/release.so".to_string(), "sha256:abc".to_string());
1202 platform_info.add_variant(
1203 "debug".to_string(),
1204 VariantInfo {
1205 library: "lib/debug.so".to_string(),
1206 checksum: "sha256:def".to_string(),
1207 build: None,
1208 },
1209 );
1210
1211 let names = platform_info.variant_names();
1212 assert_eq!(names.len(), 2);
1213 assert!(names.contains(&"release"));
1214 assert!(names.contains(&"debug"));
1215 }
1216}