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_preview(package_version),
52 Some(metadata) if metadata.starts_with("preview.") => format_preview(package_version),
53 Some(metadata) => format!("{package_version}+{metadata}"),
54 None => package_version.to_string(),
55 }
56}
57
58fn format_preview(package_version: &str) -> String {
59 if package_version.contains('-') {
60 format!("{package_version}.preview")
61 } else {
62 format!("{package_version}-preview")
63 }
64}
65
66fn format_display(semver: &str, sha: Option<&str>, date: Option<&str>) -> String {
67 match (sha.and_then(trimmed), date.and_then(trimmed)) {
68 (Some(sha), Some(date)) => format!("{semver} ({sha} {date})"),
69 _ => semver.to_string(),
70 }
71}
72
73fn trimmed(value: &str) -> Option<&str> {
74 let value = value.trim();
75 (!value.is_empty()).then_some(value)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::{format_display, format_semver};
81
82 #[test]
83 fn format_version_without_metadata_returns_package_version() {
84 assert_eq!(format_semver("1.0.0", None), "1.0.0");
85 assert_eq!(format_semver("1.0.0", Some("")), "1.0.0");
86 }
87
88 #[test]
89 fn preview_uses_prerelease_label_without_duplicate_commit() {
90 assert_eq!(format_semver("1.0.0", Some("preview")), "1.0.0-preview");
91 assert_eq!(
92 format_semver("1.0.0", Some("preview.abc1234")),
93 "1.0.0-preview"
94 );
95 assert_eq!(
96 format_semver("2.0.0-rc.1", Some("preview")),
97 "2.0.0-rc.1.preview"
98 );
99 }
100
101 #[test]
102 fn display_matches_cargo_style_when_provenance_is_available() {
103 assert_eq!(
104 format_display("1.0.0", Some("30a34c682"), Some("2026-05-25")),
105 "1.0.0 (30a34c682 2026-05-25)"
106 );
107 assert_eq!(
108 format_display("1.0.0-preview", Some("30a34c682"), Some("2026-05-25")),
109 "1.0.0-preview (30a34c682 2026-05-25)"
110 );
111 }
112
113 #[test]
114 fn display_falls_back_to_semver_when_provenance_is_incomplete() {
115 assert_eq!(format_display("1.0.0", None, None), "1.0.0");
116 assert_eq!(format_display("1.0.0", Some("30a34c682"), None), "1.0.0");
117 }
118}