1use std::{collections::BTreeMap, path::Path};
16
17use miette::{Context, IntoDiagnostic, ensure};
18use semver::Version;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21use tokio::fs;
22use url::Url;
23
24use crate::{
25 ManagedFile,
26 errors::{DeserializationError, FileExistsError, FileNotFound, SerializationError, WriteError},
27 package::{Package, PackageName},
28 registry::RegistryUri,
29};
30
31mod digest;
32pub use digest::{Digest, DigestAlgorithm};
33
34pub const LOCKFILE: &str = "Proto.lock";
36
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
41pub struct LockedDependency {
42 pub name: PackageName,
44 pub version: Version,
46}
47
48impl LockedDependency {
49 pub fn new(name: PackageName, version: Version) -> Self {
51 Self { name, version }
52 }
53}
54
55impl Serialize for LockedDependency {
57 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58 where
59 S: serde::Serializer,
60 {
61 serializer.serialize_str(&format!("{} {}", self.name, self.version))
62 }
63}
64
65impl<'de> Deserialize<'de> for LockedDependency {
67 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68 where
69 D: serde::Deserializer<'de>,
70 {
71 let s = String::deserialize(deserializer)?;
72 let parts: Vec<&str> = s.split_whitespace().collect();
73
74 if parts.len() != 2 {
75 return Err(serde::de::Error::custom(format!(
76 "invalid locked dependency format: expected 'name version', got '{}'",
77 s
78 )));
79 }
80
81 let name = PackageName::new(parts[0])
82 .map_err(|e| serde::de::Error::custom(format!("invalid package name: {}", e)))?;
83 let version = Version::parse(parts[1])
84 .map_err(|e| serde::de::Error::custom(format!("invalid version: {}", e)))?;
85
86 Ok(LockedDependency { name, version })
87 }
88}
89
90#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
94pub struct LockedPackage {
95 pub name: PackageName,
97 pub digest: Digest,
99 pub registry: RegistryUri,
101 pub repository: String,
103 pub version: Version,
105 pub dependencies: Vec<PackageName>,
107 pub dependants: usize,
111}
112
113impl LockedPackage {
114 pub fn lock(
116 package: &Package,
117 registry: RegistryUri,
118 repository: String,
119 dependants: usize,
120 ) -> Self {
121 Self {
122 name: package.name().to_owned(),
123 registry,
124 repository,
125 digest: package.digest(DigestAlgorithm::SHA256).to_owned(),
126 version: package.version().to_owned(),
127 dependencies: package
128 .manifest
129 .dependencies
130 .iter()
131 .flatten()
132 .map(|d| d.package.clone())
133 .collect(),
134 dependants,
135 }
136 }
137
138 pub fn validate(&self, package: &Package) -> miette::Result<()> {
140 let digest: Digest = DigestAlgorithm::SHA256.digest(&package.tgz);
141
142 #[derive(Error, Debug)]
143 #[error("{property} mismatch - expected {expected}, actual {actual}")]
144 struct ValidationError {
145 property: &'static str,
146 expected: String,
147 actual: String,
148 }
149
150 ensure!(
151 &self.name == package.name(),
152 ValidationError {
153 property: "name",
154 expected: self.name.to_string(),
155 actual: package.name().to_string(),
156 }
157 );
158
159 ensure!(
160 &self.version == package.version(),
161 ValidationError {
162 property: "version",
163 expected: self.version.to_string(),
164 actual: package.version().to_string(),
165 }
166 );
167
168 ensure!(
169 self.digest == digest,
170 ValidationError {
171 property: "digest",
172 expected: self.digest.to_string(),
173 actual: digest.to_string(),
174 }
175 );
176
177 Ok(())
178 }
179}
180
181impl From<&WorkspaceLockedPackage> for LockedPackage {
182 fn from(ws_locked: &WorkspaceLockedPackage) -> Self {
183 Self {
184 name: ws_locked.name.clone(),
185 version: ws_locked.version.clone(),
186 digest: ws_locked.digest.clone(),
187 registry: ws_locked.registry.clone(),
188 repository: ws_locked.repository.clone(),
189 dependencies: ws_locked
190 .dependencies
191 .iter()
192 .map(|d| d.name.clone())
193 .collect(),
194 dependants: ws_locked.dependants,
195 }
196 }
197}
198
199#[derive(Serialize, Deserialize)]
200struct RawPackageLockfile {
201 version: u16,
202 packages: Vec<LockedPackage>,
203}
204
205impl RawPackageLockfile {
206 pub fn v1(packages: Vec<LockedPackage>) -> Self {
207 Self {
208 version: 1,
209 packages,
210 }
211 }
212}
213
214#[derive(Default, Debug, PartialEq, Clone)]
218pub struct PackageLockfile {
219 packages: BTreeMap<PackageName, LockedPackage>,
220}
221
222impl PackageLockfile {
223 pub async fn exists() -> miette::Result<bool> {
225 Self::exists_at(LOCKFILE).await
226 }
227
228 pub async fn exists_at(path: impl AsRef<Path>) -> miette::Result<bool> {
230 fs::try_exists(path)
231 .await
232 .into_diagnostic()
233 .wrap_err(FileExistsError(LOCKFILE))
234 }
235
236 pub async fn read() -> miette::Result<Self> {
238 Self::read_from(LOCKFILE).await
239 }
240
241 pub async fn read_from(path: impl AsRef<Path>) -> miette::Result<Self> {
243 match fs::read_to_string(path).await {
244 Ok(contents) => {
245 let raw: RawPackageLockfile = toml::from_str(&contents)
246 .into_diagnostic()
247 .wrap_err(DeserializationError(ManagedFile::Lock))?;
248 Ok(Self::from_iter(raw.packages.into_iter()))
249 }
250 Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
251 Err(FileNotFound(LOCKFILE.into()).into())
252 }
253 Err(err) => Err(err).into_diagnostic(),
254 }
255 }
256
257 pub async fn read_or_default() -> miette::Result<Self> {
259 if PackageLockfile::exists().await? {
260 PackageLockfile::read().await
261 } else {
262 Ok(PackageLockfile::default())
263 }
264 }
265
266 pub async fn read_from_or_default(path: impl AsRef<Path>) -> miette::Result<Self> {
268 if PackageLockfile::exists_at(&path).await? {
269 PackageLockfile::read_from(path).await
270 } else {
271 Ok(PackageLockfile::default())
272 }
273 }
274
275 pub async fn write(&self, path: impl AsRef<Path>) -> miette::Result<()> {
277 let mut packages: Vec<_> = self
278 .packages
279 .values()
280 .map(|pkg| {
281 let mut locked = pkg.clone();
282 locked.dependencies.sort();
283 locked
284 })
285 .collect();
286
287 packages.sort();
288
289 let raw = RawPackageLockfile::v1(packages);
290 let lockfile_path = path.as_ref().join(LOCKFILE);
291
292 fs::write(
293 lockfile_path,
294 toml::to_string(&raw)
295 .into_diagnostic()
296 .wrap_err(SerializationError(ManagedFile::Lock))?
297 .into_bytes(),
298 )
299 .await
300 .into_diagnostic()
301 .wrap_err(WriteError(LOCKFILE))
302 }
303
304 pub fn get(&self, name: &PackageName) -> Option<&LockedPackage> {
306 self.packages.get(name)
307 }
308}
309
310impl TryFrom<Vec<WorkspaceLockedPackage>> for PackageLockfile {
311 type Error = miette::Error;
312
313 fn try_from(locked: Vec<WorkspaceLockedPackage>) -> Result<Self, Self::Error> {
314 let package_locked: Vec<LockedPackage> = locked.iter().map(LockedPackage::from).collect();
315
316 Ok(PackageLockfile::from_iter(package_locked))
317 }
318}
319
320impl FromIterator<LockedPackage> for PackageLockfile {
321 fn from_iter<I: IntoIterator<Item = LockedPackage>>(iter: I) -> Self {
322 Self {
323 packages: iter
324 .into_iter()
325 .map(|locked| (locked.name.clone(), locked))
326 .collect(),
327 }
328 }
329}
330
331#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
336pub struct WorkspaceLockedPackage {
337 pub name: PackageName,
339 pub version: Version,
341 pub digest: Digest,
343 pub registry: RegistryUri,
345 pub repository: String,
347 #[serde(default)]
349 pub dependencies: Vec<LockedDependency>,
350 pub dependants: usize,
352}
353
354impl WorkspaceLockedPackage {
355 pub fn from_locked_package(locked: LockedPackage, dependencies: Vec<LockedDependency>) -> Self {
357 Self {
358 name: locked.name,
359 version: locked.version,
360 digest: locked.digest,
361 registry: locked.registry,
362 repository: locked.repository,
363 dependencies,
364 dependants: locked.dependants,
365 }
366 }
367
368 pub fn validate(&self, package: &Package) -> miette::Result<()> {
370 let digest: Digest = DigestAlgorithm::SHA256.digest(&package.tgz);
371
372 #[derive(Error, Debug)]
373 #[error("{property} mismatch - expected {expected}, actual {actual}")]
374 struct ValidationError {
375 property: &'static str,
376 expected: String,
377 actual: String,
378 }
379
380 ensure!(
381 &self.name == package.name(),
382 ValidationError {
383 property: "name",
384 expected: self.name.to_string(),
385 actual: package.name().to_string(),
386 }
387 );
388
389 ensure!(
390 &self.version == package.version(),
391 ValidationError {
392 property: "version",
393 expected: self.version.to_string(),
394 actual: package.version().to_string(),
395 }
396 );
397
398 ensure!(
399 self.digest == digest,
400 ValidationError {
401 property: "digest",
402 expected: self.digest.to_string(),
403 actual: digest.to_string(),
404 }
405 );
406
407 Ok(())
408 }
409}
410
411#[derive(Serialize, Deserialize)]
412struct RawWorkspaceLockfile {
413 version: u16,
414 packages: Vec<WorkspaceLockedPackage>,
415}
416
417impl RawWorkspaceLockfile {
418 pub fn v1(packages: Vec<WorkspaceLockedPackage>) -> Self {
419 Self {
420 version: 1,
421 packages,
422 }
423 }
424}
425
426#[derive(Debug, PartialEq, Clone)]
432pub struct WorkspaceLockfile {
433 packages: BTreeMap<(PackageName, Version), WorkspaceLockedPackage>,
434}
435
436impl WorkspaceLockfile {
437 pub async fn exists_at(path: impl AsRef<Path>) -> miette::Result<bool> {
439 fs::try_exists(path)
440 .await
441 .into_diagnostic()
442 .wrap_err(FileExistsError(LOCKFILE))
443 }
444
445 pub async fn read_from(path: impl AsRef<Path>) -> miette::Result<Self> {
447 match fs::read_to_string(path).await {
448 Ok(contents) => {
449 let raw: RawWorkspaceLockfile = toml::from_str(&contents)
450 .into_diagnostic()
451 .wrap_err(DeserializationError(ManagedFile::Lock))?;
452 Ok(Self::from_iter(raw.packages.into_iter()))
453 }
454 Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
455 Err(FileNotFound(LOCKFILE.into()).into())
456 }
457 Err(err) => Err(err).into_diagnostic(),
458 }
459 }
460
461 pub async fn write(&self, path: impl AsRef<Path>) -> miette::Result<()> {
463 let mut packages: Vec<_> = self
464 .packages
465 .values()
466 .map(|pkg| {
467 let mut locked = pkg.clone();
468 locked.dependencies.sort();
469 locked
470 })
471 .collect();
472
473 packages.sort();
474
475 let raw = RawWorkspaceLockfile::v1(packages);
476 let lockfile_path = path.as_ref().join(LOCKFILE);
477
478 fs::write(
479 lockfile_path,
480 toml::to_string(&raw)
481 .into_diagnostic()
482 .wrap_err(SerializationError(ManagedFile::Lock))?
483 .into_bytes(),
484 )
485 .await
486 .into_diagnostic()
487 .wrap_err(WriteError(LOCKFILE))
488 }
489
490 pub fn get(&self, name: &PackageName, version: &Version) -> Option<&WorkspaceLockedPackage> {
492 self.packages.get(&(name.clone(), version.clone()))
493 }
494
495 pub fn packages(&self) -> impl Iterator<Item = &WorkspaceLockedPackage> {
497 self.packages.values()
498 }
499}
500
501#[derive(Debug, Clone)]
503pub enum Lockfile {
504 Package(PackageLockfile),
506 Workspace(WorkspaceLockfile),
508}
509
510impl Lockfile {
511 pub fn get(&self, name: &PackageName, version: &Version) -> Option<FileRequirement> {
513 match self {
514 Self::Package(lock) => lock
515 .get(name)
516 .filter(|p| p.version == *version)
517 .map(FileRequirement::from),
518 Self::Workspace(lock) => lock.get(name, version).map(FileRequirement::from),
519 }
520 }
521}
522
523impl FromIterator<WorkspaceLockedPackage> for WorkspaceLockfile {
525 fn from_iter<I: IntoIterator<Item = WorkspaceLockedPackage>>(iter: I) -> Self {
526 Self {
527 packages: iter
528 .into_iter()
529 .map(|locked| ((locked.name.clone(), locked.version.clone()), locked))
530 .collect(),
531 }
532 }
533}
534
535impl TryFrom<Vec<WorkspaceLockedPackage>> for WorkspaceLockfile {
539 type Error = miette::Report;
540
541 fn try_from(locked_packages: Vec<WorkspaceLockedPackage>) -> Result<Self, Self::Error> {
542 use std::collections::BTreeMap;
543
544 let mut workspace_packages: BTreeMap<
545 (PackageName, semver::Version),
546 WorkspaceLockedPackage,
547 > = BTreeMap::new();
548
549 for locked in locked_packages {
550 let key = (locked.name.clone(), locked.version.clone());
551
552 workspace_packages
553 .entry(key)
554 .and_modify(|existing| {
555 existing.dependants += locked.dependants;
557
558 if existing.registry != locked.registry {
560 tracing::warn!(
561 "registry mismatch for {}@{}: {} vs {}. Using first seen.",
562 locked.name,
563 locked.version,
564 existing.registry,
565 locked.registry
566 );
567 }
568 if existing.digest != locked.digest {
569 tracing::warn!(
570 "digest mismatch for {}@{}: {} vs {}. Using first seen.",
571 locked.name,
572 locked.version,
573 existing.digest,
574 locked.digest
575 );
576 }
577 if existing.dependencies != locked.dependencies {
579 tracing::warn!(
580 "dependencies mismatch for {}@{}: {:?} vs {:?}. Using first seen.",
581 locked.name,
582 locked.version,
583 existing.dependencies,
584 locked.dependencies
585 );
586 }
587 })
588 .or_insert(locked);
589 }
590
591 Ok(Self::from_iter(workspace_packages.into_values()))
592 }
593}
594
595impl From<PackageLockfile> for Vec<FileRequirement> {
596 fn from(lock: PackageLockfile) -> Self {
601 lock.packages.values().map(FileRequirement::from).collect()
602 }
603}
604
605#[derive(Serialize, Clone, PartialEq, Eq)]
609pub struct FileRequirement {
610 pub(crate) package: PackageName,
611 pub(crate) url: Url,
612 pub(crate) digest: Digest,
613}
614
615impl FileRequirement {
616 pub fn url(&self) -> &Url {
618 &self.url
619 }
620
621 pub fn new(
623 url: &RegistryUri,
624 repository: &String,
625 name: &PackageName,
626 version: &Version,
627 digest: &Digest,
628 ) -> Self {
629 let mut url = url.clone();
630 let new_path = format!(
631 "{}/{}/{}/{}-{}.tgz",
632 url.path(),
633 repository,
634 name,
635 name,
636 version
637 );
638
639 url.set_path(&new_path);
640
641 Self {
642 package: name.to_owned(),
643 url: url.into(),
644 digest: digest.clone(),
645 }
646 }
647}
648
649impl From<LockedPackage> for FileRequirement {
650 fn from(package: LockedPackage) -> Self {
651 Self::new(
652 &package.registry,
653 &package.repository,
654 &package.name,
655 &package.version,
656 &package.digest,
657 )
658 }
659}
660
661impl From<&LockedPackage> for FileRequirement {
662 fn from(package: &LockedPackage) -> Self {
663 Self::new(
664 &package.registry,
665 &package.repository,
666 &package.name,
667 &package.version,
668 &package.digest,
669 )
670 }
671}
672
673impl From<WorkspaceLockedPackage> for FileRequirement {
674 fn from(package: WorkspaceLockedPackage) -> Self {
675 Self::new(
676 &package.registry,
677 &package.repository,
678 &package.name,
679 &package.version,
680 &package.digest,
681 )
682 }
683}
684
685impl From<&WorkspaceLockedPackage> for FileRequirement {
686 fn from(package: &WorkspaceLockedPackage) -> Self {
687 Self::new(
688 &package.registry,
689 &package.repository,
690 &package.name,
691 &package.version,
692 &package.digest,
693 )
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use std::{collections::BTreeMap, str::FromStr};
700
701 use semver::Version;
702
703 use crate::{package::PackageName, registry::RegistryUri};
704
705 use super::{
706 Digest, DigestAlgorithm, FileRequirement, LockedDependency, LockedPackage, PackageLockfile,
707 WorkspaceLockedPackage, WorkspaceLockfile,
708 };
709
710 fn simple_lockfile() -> PackageLockfile {
711 PackageLockfile {
712 packages: BTreeMap::from([
713 (
714 PackageName::new("package1").unwrap(),
715 LockedPackage {
716 name: PackageName::new("package1").unwrap(),
717 digest: Digest::from_parts(
718 DigestAlgorithm::SHA256,
719 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
720 )
721 .unwrap(),
722 registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
723 repository: "my-repo".to_owned(),
724 version: Version::new(0, 1, 0),
725 dependencies: Default::default(),
726 dependants: 1,
727 },
728 ),
729 (
730 PackageName::new("package2").unwrap(),
731 LockedPackage {
732 name: PackageName::new("package2").unwrap(),
733 digest: Digest::from_parts(
734 DigestAlgorithm::SHA256,
735 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
736 )
737 .unwrap(),
738 registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
739 repository: "my-other-repo".to_owned(),
740 version: Version::new(0, 2, 0),
741 dependencies: Default::default(),
742 dependants: 1,
743 },
744 ),
745 (
746 PackageName::new("package3").unwrap(),
747 LockedPackage {
748 name: PackageName::new("package3").unwrap(),
749 digest: Digest::from_parts(
750 DigestAlgorithm::SHA256,
751 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
752 )
753 .unwrap(),
754 registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
755 repository: "your-repo".to_owned(),
756 version: Version::new(0, 2, 0),
757 dependencies: Default::default(),
758 dependants: 1,
759 },
760 ),
761 (
762 PackageName::new("package4").unwrap(),
763 LockedPackage {
764 name: PackageName::new("package4").unwrap(),
765 digest: Digest::from_parts(
766 DigestAlgorithm::SHA256,
767 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
768 )
769 .unwrap(),
770 registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
771 repository: "your-other-repo".to_owned(),
772 version: Version::new(0, 2, 0),
773 dependencies: Default::default(),
774 dependants: 1,
775 },
776 ),
777 ]),
778 }
779 }
780
781 #[test]
782 fn stable_file_requirement_order() {
783 let lock = simple_lockfile();
784 let files: Vec<FileRequirement> = lock.into();
785 for _ in 0..30 {
786 let other_files: Vec<FileRequirement> = simple_lockfile().into();
787 assert!(other_files == files)
788 }
789 }
790
791 #[tokio::test]
792 async fn test_exists_at_returns_false_for_nonexistent_file() {
793 use tempfile::TempDir;
794
795 let temp_dir = TempDir::new().unwrap();
796 let lockfile_path = temp_dir.path().join("Proto.lock");
797
798 let exists = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
799 assert!(!exists);
800 }
801
802 #[tokio::test]
803 async fn test_exists_at_returns_true_for_existing_file() {
804 use tempfile::TempDir;
805 use tokio::fs;
806
807 let temp_dir = TempDir::new().unwrap();
808 let lockfile_path = temp_dir.path().join("Proto.lock");
809
810 fs::write(&lockfile_path, "").await.unwrap();
812
813 let exists = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
814 assert!(exists);
815 }
816
817 #[tokio::test]
818 async fn test_exists_at_accepts_reference_and_owned() {
819 use std::path::PathBuf;
820 use tempfile::TempDir;
821
822 let temp_dir = TempDir::new().unwrap();
823 let lockfile_path = temp_dir.path().join("Proto.lock");
824
825 let exists_ref = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
827 assert!(!exists_ref);
828
829 let lockfile_path_owned = PathBuf::from(&lockfile_path);
831 let exists_owned = PackageLockfile::exists_at(lockfile_path_owned)
832 .await
833 .unwrap();
834 assert!(!exists_owned);
835
836 let path_str = lockfile_path.to_str().unwrap();
838 let exists_str = PackageLockfile::exists_at(path_str).await.unwrap();
839 assert!(!exists_str);
840 }
841
842 #[tokio::test]
843 async fn test_read_from_or_default_returns_default_when_file_missing() {
844 use tempfile::TempDir;
845
846 let temp_dir = TempDir::new().unwrap();
847 let lockfile_path = temp_dir.path().join("Proto.lock");
848
849 let lockfile = PackageLockfile::read_from_or_default(&lockfile_path)
850 .await
851 .unwrap();
852
853 assert_eq!(lockfile.packages.len(), 0);
854 assert_eq!(lockfile, PackageLockfile::default());
855 }
856
857 #[tokio::test]
858 async fn test_read_from_or_default_reads_existing_file() {
859 use tempfile::TempDir;
860
861 let temp_dir = TempDir::new().unwrap();
862 let lockfile_path = temp_dir.path().join("Proto.lock");
863
864 let original_lockfile = simple_lockfile();
866 original_lockfile.write(temp_dir.path()).await.unwrap();
867
868 let loaded_lockfile = PackageLockfile::read_from_or_default(&lockfile_path)
870 .await
871 .unwrap();
872
873 assert_eq!(loaded_lockfile.packages.len(), 4);
874 assert!(
875 loaded_lockfile
876 .packages
877 .contains_key(&PackageName::new("package1").unwrap())
878 );
879 assert!(
880 loaded_lockfile
881 .packages
882 .contains_key(&PackageName::new("package2").unwrap())
883 );
884 assert!(
885 loaded_lockfile
886 .packages
887 .contains_key(&PackageName::new("package3").unwrap())
888 );
889 assert!(
890 loaded_lockfile
891 .packages
892 .contains_key(&PackageName::new("package4").unwrap())
893 );
894 }
895
896 #[test]
897 fn test_locked_dependency_serialization() {
898 let deps = vec![
900 LockedDependency::new(
901 PackageName::unchecked("remote-lib-a"),
902 Version::new(1, 5, 0),
903 ),
904 LockedDependency::new(
905 PackageName::unchecked("remote-lib-b"),
906 Version::new(2, 0, 1),
907 ),
908 ];
909
910 #[derive(serde::Serialize, serde::Deserialize)]
911 struct TestWrapper {
912 dependencies: Vec<LockedDependency>,
913 }
914
915 let wrapper = TestWrapper { dependencies: deps };
916 let serialized = toml::to_string(&wrapper).unwrap();
917
918 assert!(serialized.contains("dependencies = ["));
920 assert!(serialized.contains("\"remote-lib-a 1.5.0\""));
921 assert!(serialized.contains("\"remote-lib-b 2.0.1\""));
922
923 let deserialized: TestWrapper = toml::from_str(&serialized).unwrap();
925 assert_eq!(deserialized.dependencies.len(), 2);
926 assert_eq!(
927 deserialized.dependencies[0].name,
928 PackageName::unchecked("remote-lib-a")
929 );
930 assert_eq!(deserialized.dependencies[0].version, Version::new(1, 5, 0));
931 assert_eq!(
932 deserialized.dependencies[1].name,
933 PackageName::unchecked("remote-lib-b")
934 );
935 assert_eq!(deserialized.dependencies[1].version, Version::new(2, 0, 1));
936 }
937
938 #[test]
939 fn test_workspace_lockfile_serialization() {
940 let pkg1 = WorkspaceLockedPackage {
942 name: PackageName::unchecked("remote-lib-a"),
943 version: Version::new(1, 0, 0),
944 registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
945 repository: "test-repo".to_string(),
946 digest: Digest::from_parts(
947 DigestAlgorithm::SHA256,
948 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
949 )
950 .unwrap(),
951 dependencies: vec![LockedDependency::new(
952 PackageName::unchecked("remote-lib-b"),
953 Version::new(1, 5, 0),
954 )],
955 dependants: 2,
956 };
957
958 let pkg2 = WorkspaceLockedPackage {
959 name: PackageName::unchecked("remote-lib-b"),
960 version: Version::new(1, 5, 0),
961 registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
962 repository: "test-repo".to_string(),
963 digest: Digest::from_parts(
964 DigestAlgorithm::SHA256,
965 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
966 )
967 .unwrap(),
968 dependencies: vec![], dependants: 1,
970 };
971
972 let lockfile = WorkspaceLockfile::from_iter(vec![pkg1, pkg2]);
973
974 let serialized = toml::to_string(&super::RawWorkspaceLockfile {
976 version: 1,
977 packages: lockfile.packages.values().cloned().collect(),
978 })
979 .unwrap();
980
981 assert!(serialized.contains("version = 1"));
983 assert!(serialized.contains("[[packages]]"));
984 assert!(serialized.contains("name = \"remote-lib-a\""));
985 assert!(serialized.contains("version = \"1.0.0\""));
986 assert!(serialized.contains("dependencies = [\"remote-lib-b 1.5.0\"]"));
987 assert!(serialized.contains("dependants = 2"));
988 assert!(serialized.contains("name = \"remote-lib-b\""));
989 assert!(serialized.contains("version = \"1.5.0\""));
990 assert!(serialized.contains("dependencies = []"));
991 assert!(serialized.contains("dependants = 1"));
992
993 let raw: super::RawWorkspaceLockfile = toml::from_str(&serialized).unwrap();
995 assert_eq!(raw.version, 1);
996 assert_eq!(raw.packages.len(), 2);
997
998 let restored = WorkspaceLockfile::from_iter(raw.packages);
999 assert_eq!(restored.packages.len(), 2);
1000
1001 let found = restored.get(
1003 &PackageName::unchecked("remote-lib-a"),
1004 &Version::new(1, 0, 0),
1005 );
1006 assert!(found.is_some());
1007 assert_eq!(found.unwrap().dependencies.len(), 1);
1008 }
1009
1010 #[test]
1011 fn test_workspace_lockfile_supports_multiple_versions() {
1012 let pkg_v1 = WorkspaceLockedPackage {
1014 name: PackageName::unchecked("remote-lib"),
1015 version: Version::new(1, 0, 0),
1016 registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
1017 repository: "test-repo".to_string(),
1018 digest: Digest::from_parts(
1019 DigestAlgorithm::SHA256,
1020 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
1021 )
1022 .unwrap(),
1023 dependencies: vec![],
1024 dependants: 1,
1025 };
1026
1027 let pkg_v2 = WorkspaceLockedPackage {
1028 name: PackageName::unchecked("remote-lib"),
1029 version: Version::new(2, 0, 0),
1030 registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
1031 repository: "test-repo".to_string(),
1032 digest: Digest::from_parts(
1033 DigestAlgorithm::SHA256,
1034 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
1035 )
1036 .unwrap(),
1037 dependencies: vec![],
1038 dependants: 1,
1039 };
1040
1041 let lockfile = WorkspaceLockfile::from_iter(vec![pkg_v1, pkg_v2]);
1042
1043 assert_eq!(lockfile.packages.len(), 2);
1045
1046 let v1 = lockfile.get(
1048 &PackageName::unchecked("remote-lib"),
1049 &Version::new(1, 0, 0),
1050 );
1051 assert!(v1.is_some());
1052 assert_eq!(v1.unwrap().version, Version::new(1, 0, 0));
1053
1054 let v2 = lockfile.get(
1055 &PackageName::unchecked("remote-lib"),
1056 &Version::new(2, 0, 0),
1057 );
1058 assert!(v2.is_some());
1059 assert_eq!(v2.unwrap().version, Version::new(2, 0, 0));
1060 }
1061
1062 #[test]
1063 fn test_lockfile_package_returns_file_requirement() {
1064 let lockfile = simple_lockfile();
1065 let resolved = super::Lockfile::Package(lockfile);
1066
1067 let result = resolved.get(
1069 &PackageName::new("package1").unwrap(),
1070 &Version::new(0, 1, 0),
1071 );
1072 assert!(result.is_some());
1073 let file_req = result.unwrap();
1074 assert!(file_req.url().as_str().contains("package1"));
1075
1076 let result = resolved.get(
1078 &PackageName::new("package1").unwrap(),
1079 &Version::new(9, 9, 9),
1080 );
1081 assert!(result.is_none());
1082
1083 let result = resolved.get(
1085 &PackageName::new("unknown").unwrap(),
1086 &Version::new(0, 1, 0),
1087 );
1088 assert!(result.is_none());
1089 }
1090
1091 #[test]
1092 fn test_lockfile_workspace_returns_file_requirement() {
1093 let pkg = WorkspaceLockedPackage {
1094 name: PackageName::unchecked("ws-pkg"),
1095 version: Version::new(1, 0, 0),
1096 registry: RegistryUri::from_str("https://registry.example.com").unwrap(),
1097 repository: "repo".to_string(),
1098 digest: Digest::from_parts(
1099 DigestAlgorithm::SHA256,
1100 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
1101 )
1102 .unwrap(),
1103 dependencies: vec![],
1104 dependants: 1,
1105 };
1106 let lockfile = WorkspaceLockfile::from_iter(vec![pkg]);
1107 let resolved = super::Lockfile::Workspace(lockfile);
1108
1109 let result = resolved.get(&PackageName::unchecked("ws-pkg"), &Version::new(1, 0, 0));
1111 assert!(result.is_some());
1112
1113 let result = resolved.get(&PackageName::unchecked("ws-pkg"), &Version::new(2, 0, 0));
1115 assert!(result.is_none());
1116 }
1117}