common/
version.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub type Version = String;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct BuildInfo {
8    pub version: Version,
9    pub git_hash: String,
10    pub build_profile: String,
11    pub build_features: String,
12    pub build_timestamp: String,
13    pub rust_version: String,
14    pub target: String,
15    pub host: String,
16}
17
18impl BuildInfo {
19    pub fn new() -> Self {
20        Self {
21            version: env!("CARGO_PKG_VERSION").to_string(),
22            git_hash: option_env!("REPO_VERSION").unwrap_or("unknown").to_string(),
23            build_profile: option_env!("BUILD_PROFILE")
24                .unwrap_or("unknown")
25                .to_string(),
26            build_features: option_env!("BUILD_FEATURES").unwrap_or("none").to_string(),
27            build_timestamp: option_env!("BUILD_TIMESTAMP")
28                .unwrap_or("unknown")
29                .to_string(),
30            rust_version: option_env!("RUST_VERSION").unwrap_or("unknown").to_string(),
31            target: option_env!("BUILD_TARGET").unwrap_or("unknown").to_string(),
32            host: option_env!("BUILD_HOST").unwrap_or("unknown").to_string(),
33        }
34    }
35
36    pub fn is_debug(&self) -> bool {
37        self.build_profile == "debug"
38    }
39
40    pub fn is_release(&self) -> bool {
41        self.build_profile == "release"
42    }
43
44    pub fn has_feature(&self, feature: &str) -> bool {
45        self.build_features.split(',').any(|f| f.trim() == feature)
46    }
47
48    pub fn features(&self) -> Vec<&str> {
49        if self.build_features == "none" {
50            Vec::new()
51        } else {
52            self.build_features.split(',').map(|f| f.trim()).collect()
53        }
54    }
55
56    pub fn short_hash(&self) -> &str {
57        if self.git_hash.len() > 7 {
58            &self.git_hash[..7]
59        } else {
60            &self.git_hash
61        }
62    }
63
64    pub fn is_dirty(&self) -> bool {
65        self.git_hash.contains("-dirty")
66    }
67}
68
69impl Default for BuildInfo {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl fmt::Display for BuildInfo {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(
78            f,
79            "{} ({}) built with {} on {} for {}",
80            self.version,
81            self.short_hash(),
82            self.rust_version,
83            self.build_timestamp,
84            self.target
85        )
86    }
87}
88
89#[macro_export]
90macro_rules! build_info {
91    () => {
92        $crate::version::BuildInfo::new()
93    };
94}
95
96#[macro_export]
97macro_rules! version_string {
98    () => {
99        format!(
100            "{} ({})",
101            env!("CARGO_PKG_VERSION"),
102            option_env!("REPO_VERSION").unwrap_or("unknown")
103        )
104    };
105}
106
107pub fn version() -> String {
108    version_string!()
109}
110
111pub fn build_info() -> BuildInfo {
112    build_info!()
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_build_info_creation() {
121        let info = BuildInfo::new();
122        assert!(!info.version.is_empty());
123        assert!(!info.git_hash.is_empty());
124        assert!(!info.build_profile.is_empty());
125    }
126
127    #[test]
128    fn test_version_string() {
129        let version = version();
130        assert!(version.contains(env!("CARGO_PKG_VERSION")));
131    }
132
133    #[test]
134    fn test_has_feature() {
135        let mut info = BuildInfo::new();
136        info.build_features = "serde,async,tokio".to_string();
137
138        assert!(info.has_feature("serde"));
139        assert!(info.has_feature("async"));
140        assert!(info.has_feature("tokio"));
141        assert!(!info.has_feature("nonexistent"));
142    }
143
144    #[test]
145    fn test_features_list() {
146        let mut info = BuildInfo::new();
147        info.build_features = "serde,async,tokio".to_string();
148
149        let features = info.features();
150        assert_eq!(features.len(), 3);
151        assert!(features.contains(&"serde"));
152        assert!(features.contains(&"async"));
153        assert!(features.contains(&"tokio"));
154    }
155
156    #[test]
157    fn test_no_features() {
158        let mut info = BuildInfo::new();
159        info.build_features = "none".to_string();
160
161        let features = info.features();
162        assert!(features.is_empty());
163        assert!(!info.has_feature("serde"));
164    }
165
166    #[test]
167    fn test_short_hash() {
168        let mut info = BuildInfo::new();
169        info.git_hash = "abcdef123456789".to_string();
170
171        assert_eq!(info.short_hash(), "abcdef1");
172    }
173
174    #[test]
175    fn test_is_dirty() {
176        let mut info = BuildInfo::new();
177        info.git_hash = "abcdef123456-dirty".to_string();
178
179        assert!(info.is_dirty());
180
181        info.git_hash = "abcdef123456".to_string();
182        assert!(!info.is_dirty());
183    }
184
185    #[test]
186    fn test_build_profile_checks() {
187        let mut info = BuildInfo::new();
188
189        info.build_profile = "debug".to_string();
190        assert!(info.is_debug());
191        assert!(!info.is_release());
192
193        info.build_profile = "release".to_string();
194        assert!(!info.is_debug());
195        assert!(info.is_release());
196    }
197}