1use std::sync::OnceLock;
2
3static SEMVER: OnceLock<String> = OnceLock::new();
4static DISPLAY_VERSION: OnceLock<String> = OnceLock::new();
5
6pub fn semver() -> &'static str {
7 SEMVER
8 .get_or_init(|| {
9 format_semver(
10 env!("CARGO_PKG_VERSION"),
11 option_env!("SHINE_VERSION_METADATA"),
12 )
13 })
14 .as_str()
15}
16
17pub fn display() -> &'static str {
18 DISPLAY_VERSION
19 .get_or_init(|| {
20 format_display(
21 semver(),
22 option_env!("SHINE_GIT_SHA"),
23 option_env!("SHINE_GIT_DATE"),
24 )
25 })
26 .as_str()
27}
28
29pub fn package() -> &'static str {
30 env!("CARGO_PKG_VERSION")
31}
32
33pub fn preview_commit() -> Option<&'static str> {
34 is_preview()
35 .then_some(option_env!("SHINE_GIT_SHA"))
36 .flatten()
37}
38
39fn is_preview() -> bool {
40 option_env!("SHINE_VERSION_METADATA").is_some_and(|metadata| {
41 let metadata = metadata.trim();
42 metadata == "preview" || metadata.starts_with("preview.")
43 })
44}
45
46fn format_semver(package_version: &str, metadata: Option<&str>) -> String {
47 match metadata
48 .map(str::trim)
49 .filter(|metadata| !metadata.is_empty())
50 {
51 Some("preview") => format!("{package_version}-preview"),
52 Some(metadata) if metadata.starts_with("preview.") => {
53 format!("{package_version}-preview")
54 }
55 Some(metadata) => format!("{package_version}+{metadata}"),
56 None => package_version.to_string(),
57 }
58}
59
60fn format_display(semver: &str, sha: Option<&str>, date: Option<&str>) -> String {
61 match (sha.and_then(trimmed), date.and_then(trimmed)) {
62 (Some(sha), Some(date)) => format!("{semver} ({sha} {date})"),
63 _ => semver.to_string(),
64 }
65}
66
67fn trimmed(value: &str) -> Option<&str> {
68 let value = value.trim();
69 (!value.is_empty()).then_some(value)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::{format_display, format_semver};
75
76 #[test]
77 fn format_version_without_metadata_returns_package_version() {
78 assert_eq!(format_semver("1.0.0", None), "1.0.0");
79 assert_eq!(format_semver("1.0.0", Some("")), "1.0.0");
80 }
81
82 #[test]
83 fn preview_uses_prerelease_label_without_duplicate_commit() {
84 assert_eq!(format_semver("1.0.0", Some("preview")), "1.0.0-preview");
85 assert_eq!(
86 format_semver("1.0.0", Some("preview.abc1234")),
87 "1.0.0-preview"
88 );
89 }
90
91 #[test]
92 fn display_matches_cargo_style_when_provenance_is_available() {
93 assert_eq!(
94 format_display("1.0.0", Some("30a34c682"), Some("2026-05-25")),
95 "1.0.0 (30a34c682 2026-05-25)"
96 );
97 assert_eq!(
98 format_display("1.0.0-preview", Some("30a34c682"), Some("2026-05-25")),
99 "1.0.0-preview (30a34c682 2026-05-25)"
100 );
101 }
102
103 #[test]
104 fn display_falls_back_to_semver_when_provenance_is_incomplete() {
105 assert_eq!(format_display("1.0.0", None, None), "1.0.0");
106 assert_eq!(format_display("1.0.0", Some("30a34c682"), None), "1.0.0");
107 }
108}