From 307fd1f488e6e593c42584543706d034d35448ee Mon Sep 17 00:00:00 2001 From: bill fumerola Date: Thu, 22 May 2025 15:11:39 -0700 Subject: [PATCH] feature rich --- src/semver.rs | 165 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 31 deletions(-) diff --git a/src/semver.rs b/src/semver.rs index c6a7390..64f53d2 100644 --- a/src/semver.rs +++ b/src/semver.rs @@ -48,10 +48,32 @@ pub struct VersionMap { impl VersionMap { /// Creates a new empty `VersionMap`. pub fn new() -> Self { - Self { - versions: BTreeMap::new(), - alternates: HashMap::new(), - } + Self::default() + } + + /// Returns the number of versions stored in the map. + pub fn len(&self) -> usize { + self.versions.len() + } + + /// Returns `true` if the map contains no versions. + pub fn is_empty(&self) -> bool { + self.versions.is_empty() + } + + /// Returns `true` if the map contains the specified version. + pub fn contains_version(&self, version: &Version) -> bool { + self.versions.contains_key(version) + } + + /// Returns an iterator over all versions in the map in sorted order. + pub fn versions(&self) -> impl Iterator { + self.versions.keys() + } + + /// Returns an iterator over all version-value pairs in sorted order. + pub fn iter(&self) -> impl Iterator { + self.versions.iter() } /// Attempts to insert a version-value pair, returning an error if the version already exists. @@ -60,15 +82,7 @@ impl VersionMap { return Err((version, value)); } - if let Some(alternate) = version_alternate(&version) { - self.alternates - .entry(alternate) - .or_default() - .insert(version.clone()); - } - - self.versions.insert(version, value); - + self.insert_internal(version, value); Ok(()) } @@ -76,14 +90,30 @@ impl VersionMap { /// /// Updates the alternates mapping appropriately. pub fn insert(&mut self, version: Version, value: T) -> Option { - if let Some(alternate) = version_alternate(&version) { + let old_value = self.versions.insert(version.clone(), value); + + // Only update alternates if this is a new version + if old_value.is_none() { + self.update_alternates(&version); + } + + old_value + } + + /// Internal helper for inserting without checking for existing keys. + fn insert_internal(&mut self, version: Version, value: T) { + self.update_alternates(&version); + self.versions.insert(version, value); + } + + /// Updates the alternates mapping for a given version. + fn update_alternates(&mut self, version: &Version) { + if let Some(alternate) = version_alternate(version) { self.alternates .entry(alternate) .or_default() .insert(version.clone()); } - - self.versions.insert(version, value) } /// Gets a value by version, using alternate lookup if exact match is not found. @@ -107,19 +137,23 @@ impl VersionMap { /// // Get latest major /// assert_eq!(map.get(&Version::new(1, 0, 0)), Some(&"v1.2.1")); pub fn get(&self, version: &Version) -> Option<&T> { + // Try exact match first + if let Some(value) = self.get_exact(version) { + return Some(value); + } + + // Only attempt alternate lookup for versions without build metadata if version.build.is_empty() { - let maybe_value = version_alternate(version) - .as_ref() - .and_then(|alternate| self.alternates.get(alternate)) - .and_then(|version_set| version_set.last()) - .and_then(|version| self.versions.get(version)); - - if maybe_value.is_some() { - return maybe_value; + if let Some(alternate) = version_alternate(version) { + if let Some(version_set) = self.alternates.get(&alternate) { + if let Some(latest_version) = version_set.last() { + return self.versions.get(latest_version); + } + } } } - self.get_exact(version) + None } /// Gets a value by version or returns the latest version if no specific version is provided. @@ -167,17 +201,41 @@ impl VersionMap { self.versions.get(version) } + /// Gets a mutable reference to a value by exact version match. + pub fn get_mut(&mut self, version: &Version) -> Option<&mut T> { + self.versions.get_mut(version) + } + + /// Removes a version and its associated value from the map. + /// + /// Returns the removed value if the version existed. pub fn remove(&mut self, version: &Version) -> Option { - if let Some(alternate) = version_alternate(version) { - if let Some(set) = self.alternates.get_mut(&alternate) { - set.remove(version); - if set.is_empty() { - self.alternates.remove(&alternate); + let result = self.versions.remove(version); + + if result.is_some() { + // Clean up alternates mapping + if let Some(alternate) = version_alternate(version) { + if let Some(set) = self.alternates.get_mut(&alternate) { + set.remove(version); + if set.is_empty() { + self.alternates.remove(&alternate); + } } } } - self.versions.remove(version) + result + } + + /// Clears all versions from the map. + pub fn clear(&mut self) { + self.versions.clear(); + self.alternates.clear(); + } + + /// Returns the first (lowest) version and its value. + pub fn get_first(&self) -> Option<(&Version, &T)> { + self.versions.first_key_value() } } @@ -210,6 +268,9 @@ mod tests { fn test_version_map_basic_operations() { let mut map = VersionMap::new(); + assert!(map.is_empty()); + assert_eq!(map.len(), 0); + let version0 = Version::new(0, 4, 2); let version1 = Version::new(1, 0, 0); let version2 = Version::new(1, 0, 1); @@ -221,8 +282,15 @@ mod tests { assert!(map.try_insert(version2.clone(), "value2").is_ok()); assert!(map.try_insert(version3.clone(), "value3").is_ok()); + assert_eq!(map.len(), 4); + assert!(!map.is_empty()); + // Test duplicate insertion assert!(map.try_insert(version1.clone(), "duplicate").is_err()); + + // Test contains + assert!(map.contains_version(&version1)); + assert!(!map.contains_version(&Version::new(5, 0, 0))); } #[test] @@ -271,6 +339,7 @@ mod tests { map.insert(Version::new(0, 1, 0), "v0.1.0"); assert_eq!(map.get_latest(), Some((&Version::new(2, 0, 0), &"v2.0.0"))); + assert_eq!(map.get_first(), Some((&Version::new(0, 1, 0), &"v0.1.0"))); assert_eq!(map.get_or_latest(None), Some(&"v2.0.0")); assert_eq!( map.get_or_latest(Some(&Version::new(1, 0, 0))), @@ -288,8 +357,13 @@ mod tests { map.insert(v1.clone(), "v1"); map.insert(v2.clone(), "v2"); + assert_eq!(map.len(), 2); assert_eq!(map.remove(&v1), Some("v1")); + assert_eq!(map.len(), 1); assert_eq!(map.remove(&v1), None); // Already removed + + map.clear(); + assert!(map.is_empty()); } #[test] @@ -328,4 +402,33 @@ mod tests { Some(Version::new(0, 0, 5)) ); } + + #[test] + fn test_version_map_iterators() { + let mut map = VersionMap::new(); + + map.insert(Version::new(2, 0, 0), "v2"); + map.insert(Version::new(1, 0, 0), "v1"); + map.insert(Version::new(3, 0, 0), "v3"); + + let versions: Vec<_> = map.versions().cloned().collect(); + assert_eq!( + versions, + vec![ + Version::new(1, 0, 0), + Version::new(2, 0, 0), + Version::new(3, 0, 0), + ] + ); + + let pairs: Vec<_> = map.iter().map(|(v, val)| (v.clone(), *val)).collect(); + assert_eq!( + pairs, + vec![ + (Version::new(1, 0, 0), "v1"), + (Version::new(2, 0, 0), "v2"), + (Version::new(3, 0, 0), "v3"), + ] + ); + } }