From 3ce2b975a0347168dfb895b51938e85a056e2afc Mon Sep 17 00:00:00 2001 From: Assaf Vayner Date: Thu, 24 Oct 2024 10:41:40 -0700 Subject: [PATCH] clean utils deps (#60) * keep vscode settings.json * clean deps from utils --- .gitignore | 3 +- .vscode/settings.json | 6 + Cargo.lock | 121 +------------------ utils/Cargo.toml | 28 +---- utils/build.rs | 6 - utils/examples/infra.rs | 50 -------- utils/proto/alb.proto | 7 -- utils/proto/common.proto | 43 ------- utils/proto/infra.proto | 22 ---- utils/src/consistenthash.rs | 151 ------------------------ utils/src/constants.rs | 17 --- utils/src/gitbaretools.rs | 24 ---- utils/src/lib.rs | 79 ------------- utils/src/singleflight.rs | 4 +- utils/src/version.rs | 229 ------------------------------------ 15 files changed, 15 insertions(+), 775 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 utils/build.rs delete mode 100644 utils/examples/infra.rs delete mode 100644 utils/proto/alb.proto delete mode 100644 utils/proto/common.proto delete mode 100644 utils/proto/infra.proto delete mode 100644 utils/src/consistenthash.rs delete mode 100644 utils/src/constants.rs delete mode 100644 utils/src/gitbaretools.rs delete mode 100644 utils/src/version.rs diff --git a/.gitignore b/.gitignore index 14948125..b916b776 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ debug/ .DS_Store # VS Code configs -.vscode +.vscode/* +!.vscode/settings.json venv **/*.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a687b3fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.rustfmt.extraArgs": ["+nightly"], + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + } +} diff --git a/Cargo.lock b/Cargo.lock index 6495479e..af23e7c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,17 +28,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.11" @@ -1061,15 +1050,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "colorchoice" version = "1.0.2" @@ -2304,9 +2284,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" @@ -2314,7 +2291,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash", "allocator-api2", ] @@ -2333,15 +2310,6 @@ dependencies = [ "fxhash", ] -[[package]] -name = "hashring" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bfd649ac5e0f82ae98d547450f1d31af49742be255b5380c61fc8513b9df11" -dependencies = [ - "siphasher", -] - [[package]] name = "heck" version = "0.3.3" @@ -3279,12 +3247,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" -[[package]] -name = "multimap" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" - [[package]] name = "nanorand" version = "0.7.0" @@ -4136,16 +4098,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" -dependencies = [ - "proc-macro2", - "syn 2.0.79", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4216,27 +4168,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.79", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.12.6" @@ -4250,15 +4181,6 @@ dependencies = [ "syn 2.0.79", ] -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost", -] - [[package]] name = "protobuf" version = "2.28.0" @@ -4376,19 +4298,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rand" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -6277,19 +6186,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tonic-build" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "quote", - "syn 2.0.79", -] - [[package]] name = "tower" version = "0.4.13" @@ -6572,27 +6468,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" name = "utils" version = "0.14.5" dependencies = [ - "anyhow", - "chrono", - "clap 3.2.25", "futures", - "hashbrown 0.12.3", - "hashring", - "http 0.2.12", - "itertools 0.10.5", - "lazy_static", "merklehash", "parking_lot 0.11.2", "pin-project", - "prost", - "prost-types", - "rand 0.5.6", - "regex", "serde", - "tempfile", "tokio", - "tonic", - "tonic-build", "tracing", "xet_error", ] diff --git a/utils/Cargo.toml b/utils/Cargo.toml index f34251a4..4f07b574 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -8,42 +8,22 @@ name = "utils" path = "src/lib.rs" [dependencies] -tonic = {version = "0.10.2", features = ["tls", "tls-roots", "transport"] } -prost = "0.12.3" -prost-types = "0.12.3" -serde = {version = "1.0", features = ["derive"] } -merklehash = { path = "../merklehash"} -xet_error = {path = "../xet_error"} +serde = { version = "1.0", features = ["derive"] } +merklehash = { path = "../merklehash" } +xet_error = { path = "../xet_error" } futures = "0.3.28" -tempfile = "3.9.0" # singleflight -tokio = {version = "1", features = ["sync"] } -hashbrown = "0.12.0" +tokio = { version = "1", features = ["sync"] } parking_lot = "0.11" -anyhow = "1" pin-project = "1.0.12" # consistenthash -hashring = "0.3.0" tracing = "0.1.31" -chrono = "0.4" -lazy_static = "1.4.0" -regex = "1.7.3" -[build-dependencies] -tonic-build = {version= "0.10.2", features=["transport"]} [dev-dependencies] tokio = { version = "1.36", features = ["full"] } -futures = "0.3.21" -clap = { version = "3.1.6", features = ["derive"] } -http = "0.2.5" -rand = "0.5" -itertools = "0.10" - -[[example]] -name = "infra" [features] strict = [] diff --git a/utils/build.rs b/utils/build.rs deleted file mode 100644 index 5516d623..00000000 --- a/utils/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() -> Result<(), Box> { - tonic_build::configure().compile(&["proto/common.proto"], &["proto"])?; - tonic_build::configure().compile(&["proto/infra.proto"], &["proto"])?; - tonic_build::configure().compile(&["proto/alb.proto"], &["proto"])?; - Ok(()) -} diff --git a/utils/examples/infra.rs b/utils/examples/infra.rs deleted file mode 100644 index ef292b6a..00000000 --- a/utils/examples/infra.rs +++ /dev/null @@ -1,50 +0,0 @@ -use clap::Parser; -use http::Uri; -use tonic::transport::Channel; -use utils::common::Empty; -use utils::consistenthash::ConsistentHash; -use utils::infra::infra_utils_client::InfraUtilsClient; - -pub type InfraUtilsClientType = InfraUtilsClient; -pub async fn get_infra_client(server_name: &str) -> anyhow::Result { - let mut server_uri: Uri = server_name.parse()?; - - // supports an absolute URI (above) or just the host:port (below) - if server_uri.scheme().is_none() { - server_uri = format!("https://{}", server_name).parse().unwrap(); - } - - let channel = Channel::builder(server_uri).connect().await?; - Ok(InfraUtilsClient::new(channel)) -} - -/// Simple program to greet a person -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - #[clap(short, long, value_parser)] - server_name: String, - - #[clap(short, long, value_parser)] - key: Option, -} -#[tokio::main] -async fn main() { - let args = Args::parse(); - let mut client = get_infra_client(&args.server_name).await.unwrap(); - let request = Empty {}; - let response = client.endpoint_load(request).await.unwrap(); - if let Some(key) = args.key { - let ch = ConsistentHash::new(response.into_inner().responses).unwrap(); - - println!("Key {} gets hashed to server {:?}", &key, ch.server(&key).unwrap()); - return; - } - // when no key is specified, print out the entire response - for load_status in response.into_inner().responses.into_iter() { - if let Some(load_stat) = load_status.status { - println!("Host: {}", &load_status.address); - println!("Load Stats: {:?}", load_stat); - } - } -} diff --git a/utils/proto/alb.proto b/utils/proto/alb.proto deleted file mode 100644 index 87fd8662..00000000 --- a/utils/proto/alb.proto +++ /dev/null @@ -1,7 +0,0 @@ -syntax = "proto3"; -package AWS; -import public "common.proto"; - -service ALB { - rpc healthcheck(common.Empty) returns (common.Empty); -} diff --git a/utils/proto/common.proto b/utils/proto/common.proto deleted file mode 100644 index 1d438d85..00000000 --- a/utils/proto/common.proto +++ /dev/null @@ -1,43 +0,0 @@ -syntax = "proto3"; -package common; -option go_package = "proto/"; -import "google/protobuf/timestamp.proto"; -message Key { - string prefix = 1; - bytes hash = 2; -} - -message InitiateRequest { - common.Key key = 1; - uint64 payload_size = 2; -} - -enum CompressionScheme { - NONE = 0; - LZ4 = 1; -} - -enum Scheme { - HTTP = 0; - HTTPS = 1; -} - -message EndpointConfig { - string host = 1; - int32 port = 2; - Scheme scheme = 3; - string root_ca_certificate = 4; -} - -message InitiateResponse { - // filled but deprecated in v0.2.0 clients should use host in endpoint config - string cas_hostname = 1; - - EndpointConfig data_plane_endpoint = 2; - EndpointConfig put_complete_endpoint = 3; - - repeated CompressionScheme accepted_encodings = 4; -} - -message Empty { -} diff --git a/utils/proto/infra.proto b/utils/proto/infra.proto deleted file mode 100644 index 02fad5bb..00000000 --- a/utils/proto/infra.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "proto3"; -package infra; -import public "common.proto"; - -service InfraUtils { - rpc EndpointLoad(common.Empty) returns (EndpointLoadResponse); - // Initiates uploads of an object. - rpc Initiate(common.InitiateRequest) returns (common.InitiateResponse); -} - -message SystemStatus { - string timestamp = 1; - double cpu_utilization = 2; -} -message LoadStatus { - string address = 1; - SystemStatus status = 2; -} - -message EndpointLoadResponse { - repeated LoadStatus responses = 1; -} diff --git a/utils/src/consistenthash.rs b/utils/src/consistenthash.rs deleted file mode 100644 index 7f30c97e..00000000 --- a/utils/src/consistenthash.rs +++ /dev/null @@ -1,151 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::{DateTime, Utc}; -use hashring::HashRing; -use tracing::debug; - -use crate::infra::LoadStatus; - -#[derive(Debug, Clone, Hash, PartialEq)] -struct VNode { - addr: String, -} - -impl VNode { - fn new(ip: &str) -> Self { - VNode { addr: ip.to_string() } - } -} - -pub struct ConsistentHash { - ring: HashRing, - pub ts: DateTime, -} - -impl ConsistentHash { - pub fn new(load_status_vec: Vec) -> Result { - let mut ring: HashRing = HashRing::new(); - let mut oldest_ts = Utc::now(); - let mut valid_hosts = 0; - for load_status in &load_status_vec { - if let Some(load_stat) = &load_status.status { - debug!("Host: {}", &load_status.address); - debug!("Load Stats: {:?}", load_stat); - let ts = DateTime::parse_from_rfc3339(&load_stat.timestamp)?.with_timezone(&Utc); - if oldest_ts > ts { - oldest_ts = ts; - } - ring.add(VNode::new(&load_status.address)); - valid_hosts += 1; - } - } - if valid_hosts == 0 { - return Err(anyhow!("Unable to create ConsistentHash with empty host set {:?}", load_status_vec)); - } - Ok(Self { ring, ts: oldest_ts }) - } - - pub fn server(&self, key: &str) -> Option { - self.ring.get(&key).map(|val| val.addr.to_string()) - } -} -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use chrono::{FixedOffset, TimeZone}; - use itertools::Itertools; - use rand::distributions::Alphanumeric; - use rand::{Rng, SeedableRng}; - - use crate::consistenthash::ConsistentHash; - use crate::infra::{LoadStatus, SystemStatus}; - #[test] - fn test_empty_errors() { - let res = ConsistentHash::new(vec![]); - assert!(res.is_err()); - - let infra_input = vec![LoadStatus { - address: "localhost".to_string(), - status: None, - }]; - let res = ConsistentHash::new(infra_input); - assert!(res.is_err()); - } - - #[test] - fn test_time_parsing() { - let infra_input = vec![LoadStatus { - address: "localhost".to_string(), - status: Some(SystemStatus { - timestamp: "junk".to_string(), - cpu_utilization: 1.0, - }), - }]; - let res = ConsistentHash::new(infra_input); - assert!(res.is_err()); - - let infra_input = vec![ - LoadStatus { - address: "1.1.1.1".to_string(), - status: Some(SystemStatus { - timestamp: "2022-07-06T19:15:00Z".to_string(), - cpu_utilization: 1.0, - }), - }, - LoadStatus { - address: "2.1.1.1".to_string(), - status: Some(SystemStatus { - timestamp: "2022-07-07T19:15:00Z".to_string(), - cpu_utilization: 1.0, - }), - }, - ]; - let res = ConsistentHash::new(infra_input); - assert!(res.is_ok()); - let res = res.unwrap(); - assert_eq!( - res.ts, - FixedOffset::east_opt(0) - .unwrap() - .with_ymd_and_hms(2022, 7, 6, 19, 15, 0) - .unwrap() - ); - } - #[test] - fn test_distribution() { - let infra_input = vec![ - LoadStatus { - address: "35.89.208.89".to_string(), - status: Some(SystemStatus { - timestamp: "2022-07-06T19:15:00Z".to_string(), - cpu_utilization: 1.0, - }), - }, - LoadStatus { - address: "54.245.178.249".to_string(), - status: Some(SystemStatus { - timestamp: "2022-07-07T19:15:00Z".to_string(), - cpu_utilization: 1.0, - }), - }, - ]; - let ch = ConsistentHash::new(infra_input).unwrap(); - let mut rng = rand::rngs::SmallRng::from_seed([ - 40, 219, 206, 212, 254, 181, 162, 148, 15, 114, 37, 56, 217, 149, 76, 254, - ]); - let server_counts: HashMap = (0..20) - .map(|_| { - let key: String = rng.sample_iter(&Alphanumeric).take(7).map(char::from).collect(); - ch.server(&key).unwrap() - }) - .counts(); - // wide range for testing, I am seeing typical outputs like - // Server is 35.89.208.89 and count is 11 - // Server is 54.245.178.249 and count is 9 - let expected_range = 3..18; - for (server, count) in server_counts.iter() { - println!("Server is {} and count is {}", server, count); - assert!(expected_range.contains(count)); - } - } -} diff --git a/utils/src/constants.rs b/utils/src/constants.rs deleted file mode 100644 index 4e255ec7..00000000 --- a/utils/src/constants.rs +++ /dev/null @@ -1,17 +0,0 @@ -// This file holds constants used by the server and client. -pub const AUTHORIZATION_HEADER: &str = "authorization"; -pub const USER_ID_HEADER: &str = "xet-user-id"; -pub const AUTH_HEADER: &str = "xet-auth"; -pub const REPO_PATHS_HEADER: &str = "xet-repo-paths-bin"; -pub const REQUEST_ID_HEADER: &str = "xet-request-id"; -pub const GIT_XET_VERSION_HEADER: &str = "xet-version"; -pub const TRACE_ID_HEADER: &str = "uber-trace-id"; -pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; -pub const JSON_CONTENT_TYPE: &str = "application/json"; -pub const CAS_PROTOCOL_VERSION_HEADER: &str = "xet-cas-protocol-version"; -pub const CLIENT_IP_HEADER: &str = "x-forwarded-for"; -pub const UNKNOWN_IP: &str = "0.0.0.0"; -pub const DEFAULT_USER: &str = "anonymous"; -pub const DEFAULT_AUTH: &str = "unknown"; -pub const DEFAULT_VERSION: &str = "0.0.0"; -pub const GRPC_TIMEOUT_SEC: u64 = 60; diff --git a/utils/src/gitbaretools.rs b/utils/src/gitbaretools.rs deleted file mode 100644 index 21ed4a92..00000000 --- a/utils/src/gitbaretools.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Action { - pub action: String, - pub file_path: String, - #[serde(default)] - pub previous_path: String, - #[serde(default)] - pub execute_filemode: bool, - #[serde(default)] - pub content: String, -} - -/// JSON descriptions of the manifest -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JSONCommand { - pub author_name: String, - pub author_email: String, - pub branch: String, - pub commit_message: String, - #[serde(default)] - pub create_ref: bool, - pub actions: Vec, -} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 74b2bea6..2bff7906 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,89 +1,10 @@ #![cfg_attr(feature = "strict", deny(warnings))] -// The auto code generated by tonic has clippy warnings. Disabling until those are -// resolved in tonic/prost. -pub mod common { - #![allow(clippy::derive_partial_eq_without_eq)] - tonic::include_proto!("common"); -} - -pub mod infra { - #![allow(clippy::derive_partial_eq_without_eq)] - tonic::include_proto!("infra"); -} - -pub mod alb { - #![allow(clippy::derive_partial_eq_without_eq)] - tonic::include_proto!("aws"); -} - pub mod auth; -pub mod consistenthash; -pub mod constants; pub mod errors; -pub mod gitbaretools; pub mod serialization_utils; pub mod singleflight; -pub mod version; mod output_bytes; pub use output_bytes::output_bytes; - -use crate::common::{CompressionScheme, InitiateResponse}; - -impl std::fmt::Display for common::Scheme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let as_str = match self { - common::Scheme::Http => "http", - common::Scheme::Https => "https", - }; - write!(f, "{as_str}") - } -} - -impl std::fmt::Display for common::EndpointConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let common::EndpointConfig { host, port, scheme, .. } = self; - let scheme_parsed = common::Scheme::try_from(*scheme).unwrap_or_default(); - write!(f, "{scheme_parsed}://{host}:{port}") - } -} - -impl InitiateResponse { - pub fn get_accepted_encodings_parsed(&self) -> Vec { - self.accepted_encodings - .iter() - .filter_map(|i| CompressionScheme::try_from(*i).ok()) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::common::EndpointConfig; - - #[test] - fn test_endpoint_config_to_endpoint_string() { - let host = "xetxet"; - let port = 443; - let e1 = EndpointConfig { - host: host.to_string(), - port, - scheme: common::Scheme::Http.into(), - root_ca_certificate: String::new(), - }; - - assert_eq!(e1.to_string(), format!("http://{host}:{port}")); - - let e2 = EndpointConfig { - host: host.to_string(), - port, - scheme: common::Scheme::Https.into(), - root_ca_certificate: String::from("abcd"), - }; - - assert_eq!(e2.to_string(), format!("https://{host}:{port}")); - } -} diff --git a/utils/src/singleflight.rs b/utils/src/singleflight.rs index b7ad6fa4..7bfd2e9e 100644 --- a/utils/src/singleflight.rs +++ b/utils/src/singleflight.rs @@ -35,6 +35,7 @@ //! } //! ``` +use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; use std::marker::PhantomData; @@ -44,7 +45,6 @@ use std::sync::Arc; use std::task::{ready, Context, Poll}; use futures::future::Either; -use hashbrown::HashMap; use parking_lot::RwLock; use pin_project::{pin_project, pinned_drop}; use tokio::sync::{Mutex, Notify}; @@ -349,6 +349,7 @@ where #[cfg(test)] mod tests { + use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -356,7 +357,6 @@ mod tests { use futures::future::join_all; use futures::stream::iter; use futures::StreamExt; - use hashbrown::HashMap; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::{channel, Sender}; use tokio::sync::{Mutex, Notify}; diff --git a/utils/src/version.rs b/utils/src/version.rs deleted file mode 100644 index 37723f60..00000000 --- a/utils/src/version.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::str::FromStr; - -use lazy_static::lazy_static; -use regex::{Captures, Regex}; -use xet_error::Error; - -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub struct Version { - pub major: u32, - pub minor: u32, - pub patch: u32, -} - -impl Version { - pub const fn new(major: u32, minor: u32, patch: u32) -> Self { - Self { major, minor, patch } - } -} - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum VersionError { - #[error("Could not parse {0} as version, expecting standard semantic version e.g. \"0.8.2\"")] - ParseFailed(String), -} - -const MAJOR_KEY: &str = "major"; -const MINOR_KEY: &str = "minor"; -const PATCH_KEY: &str = "patch"; - -lazy_static! { - // matching semantic versions with optional `v` at the start and optional suffix beginning with `-` - // patch version is expected but optional - // valid forms: - // 1.0.0 - // v0.9.2 - // 0.123.0-anything - // v0.0.0-multi-dash - // 0.1 - // v0.1-test - // invalid forms: - // x.0.0 - // 0.0.0- - // x7.9.0 - static ref VERSION_REGEX: Regex = Regex::new( - format!(r"^v?(?P<{MAJOR_KEY}>\d+)\.(?P<{MINOR_KEY}>\d+)(\.(?P<{PATCH_KEY}>\d+))?(-.+)?$").as_str() - ) - .unwrap(); -} - -/// util to convert regex capture group to parsable -/// captures must have named regex group and specifically have -/// the specific name given -#[inline] -fn parse_from_capture_by_key<'a, T>(captures: &Captures<'a>, name: &'a str) -> Result -where - T: FromStr, -{ - let capture = if let Some(major) = captures.name(name) { - major - } else { - return Err(VersionError::ParseFailed(format!("invalid {name}"))); - }; - - capture - .as_str() - .parse::() - .map_err(|_| VersionError::ParseFailed(format!("invalid {name}"))) -} - -/// enable version string to Version struct conversion -impl TryFrom<&str> for Version { - type Error = VersionError; - fn try_from(value: &str) -> Result { - let captures = match VERSION_REGEX.captures(value) { - Some(captures) => captures, - None => return Err(VersionError::ParseFailed(value.to_string())), - }; - - let major = parse_from_capture_by_key::(&captures, MAJOR_KEY)?; - let minor = parse_from_capture_by_key::(&captures, MINOR_KEY)?; - // allow patch to not be included - let patch = parse_from_capture_by_key::(&captures, PATCH_KEY).unwrap_or(0); - - Ok(Version { major, minor, patch }) - } -} - -/// enable version String to Version struct conversion -impl TryFrom for Version { - type Error = VersionError; - fn try_from(value: String) -> Result { - Version::try_from(value.as_str()) - } -} - -/// enable comparing versions -impl PartialOrd for Version { - fn partial_cmp(&self, other: &Self) -> Option { - match self.major.partial_cmp(&other.major) { - Some(core::cmp::Ordering::Equal) => {}, - ord => return ord, - } - match self.minor.partial_cmp(&other.minor) { - Some(core::cmp::Ordering::Equal) => {}, - ord => return ord, - } - self.patch.partial_cmp(&other.patch) - } -} - -/// allow pretty print -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Version { major, minor, patch } = self; - write!(f, "{major}.{minor}.{patch}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_version_to_string() { - let version = Version::new(1, 2, 3); - assert_eq!(version.to_string(), "1.2.3".to_string()) - } - - #[test] - fn test_version_try_from_str() { - let expected = Version::new(1, 2, 3); - - let standard_version_str = "1.2.3"; - assert_eq!(Version::try_from(standard_version_str).unwrap(), expected); - } - - #[test] - fn test_version_try_from_str_special_cases() { - let expected = Version::new(1, 2, 0); - - let no_patch = "1.2"; - assert_eq!(Version::try_from(no_patch).unwrap(), expected); - - let with_v = "v1.2.0"; - assert_eq!(Version::try_from(with_v).unwrap(), expected); - - let with_suffix = "v1.2.0-abc"; - assert_eq!(Version::try_from(with_suffix).unwrap(), expected); - - let with_v = "v1.2.0-asdasd-adsda"; - assert_eq!(Version::try_from(with_v).unwrap(), expected); - } - - #[test] - fn test_version_try_from_str_failure() { - let bad_all_over = "bad string"; - assert!(Version::try_from(bad_all_over).is_err()); - - let bad_major = "a.2.3"; - assert!(Version::try_from(bad_major).is_err()); - - let bad_minor = "1.a.3"; - assert!(Version::try_from(bad_minor).is_err()); - - let bad_patch = "1.2.a"; - assert!(Version::try_from(bad_patch).is_err()); - - let bad_suffix = "1.2.3aaa"; - assert!(Version::try_from(bad_suffix).is_err()); - - let bad_prefix = "p1.2."; - assert!(Version::try_from(bad_prefix).is_err()); - } - - #[test] - fn test_version_default() { - assert_eq!(Version::default(), Version::new(0, 0, 0)); - } - - #[test] - fn test_version_comparison() { - let zeros = Version::default(); - let ones = Version::new(1, 1, 1); - let ninties = Version::new(90, 90, 90); - - // 0.0.0 < 1.1.1 && 1.1.1 < 90.90.90 && 0.0.0 < 90.90.90 - assert!(zeros < ones); - assert!(ones < ninties); - assert!(zeros < ninties); - - // 0.0.0 < 0.1.1 && 0.1.1 < 1.1.1 - let zero_one_one = Version::new(0, 1, 1); - assert!(zeros < zero_one_one); - assert!(zero_one_one < ones); - - // 0.1.0 < 0.1.1 - let zero_one_zero = Version::new(0, 1, 0); - assert!(zero_one_zero < zero_one_one); - - // 1.0.0 > 0.1.0 && 1.0.0 > 0.1.1 - let one_zero_zero = Version::new(1, 0, 0); - assert!(one_zero_zero > zero_one_zero); - assert!(one_zero_zero > zero_one_one); - } - - #[test] - fn test_version_range() { - let zeros = Version::new(0, 0, 0); - let o_nine_o = Version::new(0, 9, 0); - let ones = Version::new(1, 1, 1); - let ninties = Version::new(90, 90, 90); - let ninety_ninety_ninety_one = Version::new(90, 90, 90); - - let range = o_nine_o..ninties; - assert!(!range.contains(&zeros)); - assert!(range.contains(&o_nine_o)); - assert!(range.contains(&ones)); - assert!(!range.contains(&ninties)); - assert!(!range.contains(&ninety_ninety_ninety_one)); - - let range_inclusive = o_nine_o..=ninties; - assert!(!range.contains(&zeros)); - assert!(range_inclusive.contains(&o_nine_o)); - assert!(range_inclusive.contains(&ones)); - assert!(range_inclusive.contains(&ninties)); - assert!(!range.contains(&ninety_ninety_ninety_one)); - } -}