stoolap/common/
version.rs

1// Copyright 2025 Stoolap Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Version information for Stoolap
16//!
17//!
18//! This module provides version constants and build information.
19
20/// Full version string from Cargo.toml (single source of truth)
21pub const VERSION: &str = env!("CARGO_PKG_VERSION");
22
23/// Major version number (parsed at runtime for compatibility)
24pub const MAJOR: u32 = parse_version_component(env!("CARGO_PKG_VERSION_MAJOR"));
25
26/// Minor version number (parsed at runtime for compatibility)
27pub const MINOR: u32 = parse_version_component(env!("CARGO_PKG_VERSION_MINOR"));
28
29/// Patch version number (parsed at runtime for compatibility)
30pub const PATCH: u32 = parse_version_component(env!("CARGO_PKG_VERSION_PATCH"));
31
32/// Parse version component at compile time
33const fn parse_version_component(s: &str) -> u32 {
34    let bytes = s.as_bytes();
35    let mut result: u32 = 0;
36    let mut i = 0;
37    while i < bytes.len() {
38        let digit = bytes[i] - b'0';
39        result = result * 10 + digit as u32;
40        i += 1;
41    }
42    result
43}
44
45/// Git commit hash at build time
46/// Set via STOOLAP_GIT_COMMIT environment variable during compilation
47pub const GIT_COMMIT: &str = match option_env!("STOOLAP_GIT_COMMIT") {
48    Some(commit) => commit,
49    None => "unknown",
50};
51
52/// Build timestamp
53/// Set via STOOLAP_BUILD_TIME environment variable during compilation
54pub const BUILD_TIME: &str = match option_env!("STOOLAP_BUILD_TIME") {
55    Some(time) => time,
56    None => "unknown",
57};
58
59/// Returns the full version string
60pub fn version() -> &'static str {
61    VERSION
62}
63
64/// Returns version info as a formatted string
65pub fn version_info() -> String {
66    format!(
67        "stoolap {} (commit: {}, built: {})",
68        VERSION, GIT_COMMIT, BUILD_TIME
69    )
70}
71
72/// Semantic version components
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct SemVer {
75    pub major: u32,
76    pub minor: u32,
77    pub patch: u32,
78}
79
80impl SemVer {
81    /// Create a new SemVer from components
82    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
83        Self {
84            major,
85            minor,
86            patch,
87        }
88    }
89
90    /// Get the current version as SemVer
91    pub const fn current() -> Self {
92        Self::new(MAJOR, MINOR, PATCH)
93    }
94
95    /// Check if this version is compatible with another version
96    /// Compatible means same major version and minor >= other.minor
97    pub fn is_compatible_with(&self, other: &SemVer) -> bool {
98        self.major == other.major && self.minor >= other.minor
99    }
100}
101
102impl std::fmt::Display for SemVer {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
105    }
106}
107
108impl std::str::FromStr for SemVer {
109    type Err = String;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        let parts: Vec<&str> = s.split('.').collect();
113        if parts.len() != 3 {
114            return Err(format!("invalid version format: {}", s));
115        }
116
117        let major = parts[0]
118            .parse()
119            .map_err(|_| format!("invalid major version: {}", parts[0]))?;
120        let minor = parts[1]
121            .parse()
122            .map_err(|_| format!("invalid minor version: {}", parts[1]))?;
123        let patch = parts[2]
124            .parse()
125            .map_err(|_| format!("invalid patch version: {}", parts[2]))?;
126
127        Ok(SemVer::new(major, minor, patch))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_version_string() {
137        // Version string should match the constants
138        let expected = format!("{}.{}.{}", MAJOR, MINOR, PATCH);
139        assert_eq!(version(), expected);
140    }
141
142    #[test]
143    fn test_version_info() {
144        let info = version_info();
145        assert!(info.contains("stoolap"));
146        assert!(info.contains(version()));
147    }
148
149    #[test]
150    fn test_git_commit_default() {
151        // Without env var set, should be "unknown"
152        // This tests the default case
153        assert!(!GIT_COMMIT.is_empty());
154    }
155
156    #[test]
157    fn test_build_time_default() {
158        // Without env var set, should be "unknown"
159        assert!(!BUILD_TIME.is_empty());
160    }
161
162    #[test]
163    fn test_semver_new() {
164        let v = SemVer::new(1, 2, 3);
165        assert_eq!(v.major, 1);
166        assert_eq!(v.minor, 2);
167        assert_eq!(v.patch, 3);
168    }
169
170    #[test]
171    fn test_semver_current() {
172        let v = SemVer::current();
173        assert_eq!(v.major, MAJOR);
174        assert_eq!(v.minor, MINOR);
175        assert_eq!(v.patch, PATCH);
176    }
177
178    #[test]
179    fn test_semver_display() {
180        let v = SemVer::new(1, 2, 3);
181        assert_eq!(v.to_string(), "1.2.3");
182    }
183
184    #[test]
185    fn test_semver_from_str() {
186        let v: SemVer = "1.2.3".parse().unwrap();
187        assert_eq!(v, SemVer::new(1, 2, 3));
188
189        // Current version should parse correctly
190        let v: SemVer = version().parse().unwrap();
191        assert_eq!(v, SemVer::current());
192    }
193
194    #[test]
195    fn test_semver_from_str_invalid() {
196        assert!("1.2".parse::<SemVer>().is_err());
197        assert!("1.2.3.4".parse::<SemVer>().is_err());
198        assert!("a.b.c".parse::<SemVer>().is_err());
199        assert!("".parse::<SemVer>().is_err());
200    }
201
202    #[test]
203    fn test_semver_compatibility() {
204        let v1 = SemVer::new(1, 2, 0);
205        let v2 = SemVer::new(1, 1, 0);
206        let v3 = SemVer::new(1, 3, 0);
207        let v4 = SemVer::new(2, 0, 0);
208
209        // v1 (1.2.0) is compatible with v2 (1.1.0) - same major, higher minor
210        assert!(v1.is_compatible_with(&v2));
211
212        // v1 (1.2.0) is NOT compatible with v3 (1.3.0) - needs higher minor
213        assert!(!v1.is_compatible_with(&v3));
214
215        // v1 (1.2.0) is NOT compatible with v4 (2.0.0) - different major
216        assert!(!v1.is_compatible_with(&v4));
217
218        // Same version is always compatible
219        assert!(v1.is_compatible_with(&v1));
220    }
221}