Extract `SemanticVersion` into its own crate (#9956)

Marshall Bowers created

This PR extracts the `SemanticVersion` out of `util` and into its own
`SemanticVersion` crate.

This allows for making use of `SemanticVersion` without needing to pull
in some of the heavier dependencies included in the `util` crate.

As part of this the public API for `SemanticVersion` has been tidied up
a bit.

Release Notes:

- N/A

Change summary

Cargo.lock                                             | 14 ++
Cargo.toml                                             |  2 
crates/collab/Cargo.toml                               |  1 
crates/collab/src/api/events.rs                        | 68 ++++++------
crates/collab/src/api/ips_file.rs                      |  5 
crates/collab/src/rpc.rs                               |  3 
crates/collab/src/rpc/connection_pool.rs               |  6 
crates/collab/src/tests/test_server.rs                 |  4 
crates/extension/Cargo.toml                            |  1 
crates/extension/src/extension_manifest.rs             |  2 
crates/extension/src/extension_store.rs                |  2 
crates/extension/src/wasm_host.rs                      | 13 +-
crates/extension/src/wasm_host/wit.rs                  |  2 
crates/extension/src/wasm_host/wit/v0_0_1.rs           |  8 -
crates/extension/src/wasm_host/wit/v0_0_4.rs           |  9 -
crates/extensions_ui/Cargo.toml                        |  1 
crates/extensions_ui/src/extension_version_selector.rs |  3 
crates/gpui/Cargo.toml                                 |  1 
crates/gpui/src/platform.rs                            |  2 
crates/gpui/src/platform/linux/platform.rs             | 12 -
crates/gpui/src/platform/mac/platform.rs               | 10 
crates/gpui/src/platform/windows/platform.rs           | 23 ++-
crates/semantic_version/Cargo.toml                     | 16 ++
crates/semantic_version/LICENSE-APACHE                 |  1 
crates/semantic_version/src/semantic_version.rs        | 42 +++++--
crates/telemetry_events/Cargo.toml                     |  2 
crates/telemetry_events/src/telemetry_events.rs        |  2 
crates/util/src/util.rs                                |  2 
28 files changed, 147 insertions(+), 110 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2265,6 +2265,7 @@ dependencies = [
  "rustc-demangle",
  "scrypt",
  "sea-orm",
+ "semantic_version",
  "semver",
  "serde",
  "serde_derive",
@@ -3558,6 +3559,7 @@ dependencies = [
  "parking_lot",
  "project",
  "schemars",
+ "semantic_version",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -3610,6 +3612,7 @@ dependencies = [
  "language",
  "picker",
  "project",
+ "semantic_version",
  "serde",
  "settings",
  "smallvec",
@@ -4446,6 +4449,7 @@ dependencies = [
  "resvg",
  "schemars",
  "seahash",
+ "semantic_version",
  "serde",
  "serde_derive",
  "serde_json",
@@ -8496,6 +8500,14 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
 
+[[package]]
+name = "semantic_version"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "serde",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.18"
@@ -9659,8 +9671,8 @@ dependencies = [
 name = "telemetry_events"
 version = "0.1.0"
 dependencies = [
+ "semantic_version",
  "serde",
- "util",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -70,6 +70,7 @@ members = [
     "crates/task",
     "crates/tasks_ui",
     "crates/search",
+    "crates/semantic_version",
     "crates/settings",
     "crates/snippet",
     "crates/sqlez",
@@ -184,6 +185,7 @@ rpc = { path = "crates/rpc" }
 task = { path = "crates/task" }
 tasks_ui = { path = "crates/tasks_ui" }
 search = { path = "crates/search" }
+semantic_version = { path = "crates/semantic_version" }
 settings = { path = "crates/settings" }
 snippet = { path = "crates/snippet" }
 sqlez = { path = "crates/sqlez" }

crates/collab/Cargo.toml 🔗

@@ -46,6 +46,7 @@ reqwest = { version = "0.11", features = ["json"] }
 rpc.workspace = true
 scrypt = "0.7"
 sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
+semantic_version.workspace = true
 semver.workspace = true
 serde.workspace = true
 serde_derive.workspace = true

crates/collab/src/api/events.rs 🔗

@@ -10,6 +10,7 @@ use axum::{
     Extension, Router, TypedHeader,
 };
 use rpc::ExtensionMetadata;
+use semantic_version::SemanticVersion;
 use serde::{Serialize, Serializer};
 use sha2::{Digest, Sha256};
 use std::sync::{Arc, OnceLock};
@@ -17,7 +18,6 @@ use telemetry_events::{
     ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
     EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
 };
-use util::SemanticVersion;
 
 pub fn router() -> Router {
     Router::new()
@@ -528,9 +528,9 @@ impl EditorEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             os_name: body.os_name.clone(),
             os_version: body.os_version.clone().unwrap_or_default(),
@@ -590,9 +590,9 @@ impl CopilotEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             os_name: body.os_name.clone(),
             os_version: body.os_version.clone().unwrap_or_default(),
@@ -645,9 +645,9 @@ impl CallEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone().unwrap_or_default(),
             session_id: body.session_id.clone(),
@@ -694,9 +694,9 @@ impl AssistantEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -738,9 +738,9 @@ impl CpuEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -785,9 +785,9 @@ impl MemoryEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -831,9 +831,9 @@ impl AppEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -876,9 +876,9 @@ impl SettingEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -927,9 +927,9 @@ impl ExtensionEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -991,9 +991,9 @@ impl EditEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),
@@ -1040,9 +1040,9 @@ impl ActionEventRow {
 
         Self {
             app_version: body.app_version.clone(),
-            major: semver.map(|s| s.major as i32),
-            minor: semver.map(|s| s.minor as i32),
-            patch: semver.map(|s| s.patch as i32),
+            major: semver.map(|v| v.major() as i32),
+            minor: semver.map(|v| v.minor() as i32),
+            patch: semver.map(|v| v.patch() as i32),
             release_channel: body.release_channel.clone().unwrap_or_default(),
             installation_id: body.installation_id.clone(),
             session_id: body.session_id.clone(),

crates/collab/src/api/ips_file.rs 🔗

@@ -1,9 +1,8 @@
 use collections::HashMap;
 
-use serde_derive::Deserialize;
-use serde_derive::Serialize;
+use semantic_version::SemanticVersion;
+use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use util::SemanticVersion;
 
 #[derive(Debug)]
 pub struct IpsFile {

crates/collab/src/rpc.rs 🔗

@@ -46,6 +46,7 @@ use rpc::{
     },
     Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
 };
+use semantic_version::SemanticVersion;
 use serde::{Serialize, Serializer};
 use std::{
     any::TypeId,
@@ -68,7 +69,7 @@ use tracing::{
     field::{self},
     info_span, instrument, Instrument,
 };
-use util::{http::IsahcHttpClient, SemanticVersion};
+use util::http::IsahcHttpClient;
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 

crates/collab/src/rpc/connection_pool.rs 🔗

@@ -2,9 +2,10 @@ use crate::db::{ChannelId, ChannelRole, UserId};
 use anyhow::{anyhow, Result};
 use collections::{BTreeMap, HashMap, HashSet};
 use rpc::ConnectionId;
+use semantic_version::SemanticVersion;
 use serde::Serialize;
+use std::fmt;
 use tracing::instrument;
-use util::{semver, SemanticVersion};
 
 #[derive(Default, Serialize)]
 pub struct ConnectionPool {
@@ -20,7 +21,6 @@ struct ConnectedUser {
 
 #[derive(Debug, Serialize)]
 pub struct ZedVersion(pub SemanticVersion);
-use std::fmt;
 
 impl fmt::Display for ZedVersion {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -30,7 +30,7 @@ impl fmt::Display for ZedVersion {
 
 impl ZedVersion {
     pub fn can_collaborate(&self) -> bool {
-        self.0 >= semver(0, 127, 3)
+        self.0 >= SemanticVersion::new(0, 127, 3)
     }
 }
 

crates/collab/src/tests/test_server.rs 🔗

@@ -19,7 +19,6 @@ use futures::{channel::oneshot, StreamExt as _};
 use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
 use language::LanguageRegistry;
 use node_runtime::FakeNodeRuntime;
-
 use notifications::NotificationStore;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
@@ -27,6 +26,7 @@ use rpc::{
     proto::{self, ChannelRole},
     RECEIVE_TIMEOUT,
 };
+use semantic_version::SemanticVersion;
 use serde_json::json;
 use settings::SettingsStore;
 use std::{
@@ -39,7 +39,7 @@ use std::{
         Arc,
     },
 };
-use util::{http::FakeHttpClient, SemanticVersion};
+use util::http::FakeHttpClient;
 use workspace::{Workspace, WorkspaceId, WorkspaceStore};
 
 pub struct TestServer {

crates/extension/Cargo.toml 🔗

@@ -33,6 +33,7 @@ lsp.workspace = true
 node_runtime.workspace = true
 project.workspace = true
 schemars.workspace = true
+semantic_version.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/extension/src/extension_manifest.rs 🔗

@@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
 use collections::BTreeMap;
 use fs::Fs;
 use language::LanguageServerName;
+use semantic_version::SemanticVersion;
 use serde::{Deserialize, Serialize};
 use std::{
     ffi::OsStr,
@@ -9,7 +10,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::SemanticVersion;
 
 /// This is the old version of the extension manifest, from when it was `extension.json`.
 #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]

crates/extension/src/extension_store.rs 🔗

@@ -33,6 +33,7 @@ use language::{
     QUERY_FILENAME_PREFIXES,
 };
 use node_runtime::NodeRuntime;
+use semantic_version::SemanticVersion;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::str::FromStr;
@@ -44,7 +45,6 @@ use std::{
 };
 use theme::{ThemeRegistry, ThemeSettings};
 use url::Url;
-use util::SemanticVersion;
 use util::{
     http::{AsyncBody, HttpClient, HttpClientWithUrl},
     maybe,

crates/extension/src/wasm_host.rs 🔗

@@ -14,11 +14,12 @@ use futures::{
 use gpui::BackgroundExecutor;
 use language::LanguageRegistry;
 use node_runtime::NodeRuntime;
+use semantic_version::SemanticVersion;
 use std::{
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
-use util::{http::HttpClient, SemanticVersion};
+use util::http::HttpClient;
 use wasmtime::{
     component::{Component, ResourceTable},
     Engine, Store,
@@ -203,11 +204,11 @@ pub fn parse_wasm_extension_version(
 
 fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
     if data.len() == 6 {
-        Some(SemanticVersion {
-            major: u16::from_be_bytes([data[0], data[1]]) as _,
-            minor: u16::from_be_bytes([data[2], data[3]]) as _,
-            patch: u16::from_be_bytes([data[4], data[5]]) as _,
-        })
+        Some(SemanticVersion::new(
+            u16::from_be_bytes([data[0], data[1]]) as _,
+            u16::from_be_bytes([data[2], data[3]]) as _,
+            u16::from_be_bytes([data[4], data[5]]) as _,
+        ))
     } else {
         None
     }

crates/extension/src/wasm_host/wit.rs 🔗

@@ -4,8 +4,8 @@ mod v0_0_4;
 use super::{wasm_engine, WasmState};
 use anyhow::{Context, Result};
 use language::LspAdapterDelegate;
+use semantic_version::SemanticVersion;
 use std::sync::Arc;
-use util::SemanticVersion;
 use wasmtime::{
     component::{Component, Instance, Linker, Resource},
     Store,

crates/extension/src/wasm_host/wit/v0_0_1.rs 🔗

@@ -3,15 +3,11 @@ use crate::wasm_host::WasmState;
 use anyhow::Result;
 use async_trait::async_trait;
 use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
+use semantic_version::SemanticVersion;
 use std::sync::{Arc, OnceLock};
-use util::SemanticVersion;
 use wasmtime::component::{Linker, Resource};
 
-pub const VERSION: SemanticVersion = SemanticVersion {
-    major: 0,
-    minor: 0,
-    patch: 1,
-};
+pub const VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
 
 wasmtime::component::bindgen!({
     async: true,

crates/extension/src/wasm_host/wit/v0_0_4.rs 🔗

@@ -5,19 +5,16 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use futures::io::BufReader;
 use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
+use semantic_version::SemanticVersion;
 use std::{
     env,
     path::PathBuf,
     sync::{Arc, OnceLock},
 };
-use util::{maybe, SemanticVersion};
+use util::maybe;
 use wasmtime::component::{Linker, Resource};
 
-pub const VERSION: SemanticVersion = SemanticVersion {
-    major: 0,
-    minor: 0,
-    patch: 4,
-};
+pub const VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
 
 wasmtime::component::bindgen!({
     async: true,

crates/extensions_ui/Cargo.toml 🔗

@@ -26,6 +26,7 @@ gpui.workspace = true
 language.workspace = true
 picker.workspace = true
 project.workspace = true
+semantic_version.workspace = true
 serde.workspace = true
 settings.workspace = true
 smallvec.workspace = true

crates/extensions_ui/src/extension_version_selector.rs 🔗

@@ -9,9 +9,10 @@ use gpui::{
     prelude::*, AppContext, DismissEvent, EventEmitter, FocusableView, Task, View, WeakView,
 };
 use picker::{Picker, PickerDelegate};
+use semantic_version::SemanticVersion;
 use settings::update_settings_file;
 use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
-use util::{ResultExt, SemanticVersion};
+use util::ResultExt;
 use workspace::ModalView;
 
 pub struct ExtensionVersionSelector {

crates/gpui/Cargo.toml 🔗

@@ -57,6 +57,7 @@ refineable.workspace = true
 resvg = "0.40"
 schemars.workspace = true
 seahash = "4.1"
+semantic_version.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/gpui/src/platform.rs 🔗

@@ -53,10 +53,10 @@ pub use keystroke::*;
 pub(crate) use linux::*;
 #[cfg(target_os = "macos")]
 pub(crate) use mac::*;
+pub use semantic_version::SemanticVersion;
 #[cfg(any(test, feature = "test-support"))]
 pub(crate) use test::*;
 use time::UtcOffset;
-pub use util::SemanticVersion;
 #[cfg(target_os = "windows")]
 pub(crate) use windows::*;
 

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -353,19 +353,11 @@ impl Platform for LinuxPlatform {
     }
 
     fn os_version(&self) -> Result<SemanticVersion> {
-        Ok(SemanticVersion {
-            major: 1,
-            minor: 0,
-            patch: 0,
-        })
+        Ok(SemanticVersion::new(1, 0, 0))
     }
 
     fn app_version(&self) -> Result<SemanticVersion> {
-        Ok(SemanticVersion {
-            major: 1,
-            minor: 0,
-            patch: 0,
-        })
+        Ok(SemanticVersion::new(1, 0, 0))
     }
 
     fn app_path(&self) -> Result<PathBuf> {

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -718,11 +718,11 @@ impl Platform for MacPlatform {
         unsafe {
             let process_info = NSProcessInfo::processInfo(nil);
             let version = process_info.operatingSystemVersion();
-            Ok(SemanticVersion {
-                major: version.majorVersion as usize,
-                minor: version.minorVersion as usize,
-                patch: version.patchVersion as usize,
-            })
+            Ok(SemanticVersion::new(
+                version.majorVersion as usize,
+                version.minorVersion as usize,
+                version.patchVersion as usize,
+            ))
         }
     }
 

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -12,13 +12,14 @@ use std::{
     sync::{Arc, OnceLock},
 };
 
-use ::util::{ResultExt, SemanticVersion};
+use ::util::ResultExt;
 use anyhow::{anyhow, Context, Result};
 use async_task::Runnable;
 use copypasta::{ClipboardContext, ClipboardProvider};
 use futures::channel::oneshot::{self, Receiver};
 use itertools::Itertools;
 use parking_lot::{Mutex, RwLock};
+use semantic_version::SemanticVersion;
 use smallvec::SmallVec;
 use time::UtcOffset;
 use windows::{
@@ -513,11 +514,11 @@ impl Platform for WindowsPlatform {
         let mut info = unsafe { std::mem::zeroed() };
         let status = unsafe { RtlGetVersion(&mut info) };
         if status.is_ok() {
-            Ok(SemanticVersion {
-                major: info.dwMajorVersion as _,
-                minor: info.dwMinorVersion as _,
-                patch: info.dwBuildNumber as _,
-            })
+            Ok(SemanticVersion::new(
+                info.dwMajorVersion as _,
+                info.dwMinorVersion as _,
+                info.dwBuildNumber as _,
+            ))
         } else {
             Err(anyhow::anyhow!(
                 "unable to get Windows version: {}",
@@ -606,11 +607,11 @@ impl Platform for WindowsPlatform {
         let version_info = unsafe { &*(version_info_raw as *mut VS_FIXEDFILEINFO) };
         // https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
         if version_info.dwSignature == 0xFEEF04BD {
-            return Ok(SemanticVersion {
-                major: ((version_info.dwProductVersionMS >> 16) & 0xFFFF) as usize,
-                minor: (version_info.dwProductVersionMS & 0xFFFF) as usize,
-                patch: ((version_info.dwProductVersionLS >> 16) & 0xFFFF) as usize,
-            });
+            return Ok(SemanticVersion::new(
+                ((version_info.dwProductVersionMS >> 16) & 0xFFFF) as usize,
+                (version_info.dwProductVersionMS & 0xFFFF) as usize,
+                ((version_info.dwProductVersionLS >> 16) & 0xFFFF) as usize,
+            ));
         } else {
             log::error!(
                 "no version info present: {}",

crates/semantic_version/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "semantic_version"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/semantic_version.rs"
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true

crates/util/src/semantic_version.rs → crates/semantic_version/src/semantic_version.rs 🔗

@@ -1,3 +1,7 @@
+//! Constructs for working with [semantic versions](https://semver.org/).
+
+#![deny(missing_docs)]
+
 use std::{
     fmt::{self, Display},
     str::FromStr,
@@ -6,34 +10,46 @@ use std::{
 use anyhow::{anyhow, Result};
 use serde::{de::Error, Deserialize, Serialize};
 
-/// A datastructure representing a semantic version number
+/// A [semantic version](https://semver.org/) number.
 #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
 pub struct SemanticVersion {
-    pub major: usize,
-    pub minor: usize,
-    pub patch: usize,
-}
-
-pub fn semver(major: usize, minor: usize, patch: usize) -> SemanticVersion {
-    SemanticVersion {
-        major,
-        minor,
-        patch,
-    }
+    major: usize,
+    minor: usize,
+    patch: usize,
 }
 
 impl SemanticVersion {
-    pub fn new(major: usize, minor: usize, patch: usize) -> Self {
+    /// Returns a new [`SemanticVersion`] from the given components.
+    pub const fn new(major: usize, minor: usize, patch: usize) -> Self {
         Self {
             major,
             minor,
             patch,
         }
     }
+
+    /// Returns the major version number.
+    #[inline(always)]
+    pub fn major(&self) -> usize {
+        self.major
+    }
+
+    /// Returns the minor version number.
+    #[inline(always)]
+    pub fn minor(&self) -> usize {
+        self.minor
+    }
+
+    /// Returns the patch version number.
+    #[inline(always)]
+    pub fn patch(&self) -> usize {
+        self.patch
+    }
 }
 
 impl FromStr for SemanticVersion {
     type Err = anyhow::Error;
+
     fn from_str(s: &str) -> Result<Self> {
         let mut components = s.trim().split('.');
         let major = components

crates/telemetry_events/Cargo.toml 🔗

@@ -12,5 +12,5 @@ workspace = true
 path = "src/telemetry_events.rs"
 
 [dependencies]
+semantic_version.workspace = true
 serde.workspace = true
-util.workspace = true

crates/telemetry_events/src/telemetry_events.rs 🔗

@@ -1,6 +1,6 @@
+use semantic_version::SemanticVersion;
 use serde::{Deserialize, Serialize};
 use std::{fmt::Display, sync::Arc};
-use util::SemanticVersion;
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct EventRequestBody {

crates/util/src/util.rs 🔗

@@ -3,14 +3,12 @@ pub mod fs;
 pub mod github;
 pub mod http;
 pub mod paths;
-mod semantic_version;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
 use futures::Future;
 use lazy_static::lazy_static;
 use rand::{seq::SliceRandom, Rng};
-pub use semantic_version::*;
 use std::{
     borrow::Cow,
     cmp::{self, Ordering},