cranpose_services/app_info.rs
1//! How the platform packaged this app: the version a user is shown, and the
2//! build version a store orders releases by.
3//!
4//! An app that prints its own version usually knows it at compile time, which
5//! is fine right up until the packaging step adds something the compiler never
6//! saw — an Android `versionNameSuffix`, a CI build number, a store-assigned
7//! build. Then the About screen and the artifact disagree, and the mismatch is
8//! invisible until someone reads a bug report. This asks the platform what it
9//! actually shipped.
10//!
11//! ```rust,no_run
12//! use cranpose_services::app_info;
13//!
14//! // Prefer what the platform packaged; fall back to what was compiled in.
15//! let version = app_info::version_name().unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
16//! ```
17//!
18//! The default implementation answers `None` for both: a desktop binary run
19//! straight out of `target/` was not packaged by anything and has no answer to
20//! give. Platform backends install a real one with
21//! [`set_platform_app_info`].
22
23use std::{cell::RefCell, rc::Rc};
24
25/// Reports the packaged identity of the running app.
26pub trait AppInfo {
27 /// The version a user is shown — Android's `versionName`, Apple's
28 /// `CFBundleShortVersionString`. `None` when the platform has none.
29 fn version_name(&self) -> Option<String>;
30
31 /// The build identifier a store orders releases by — Android's
32 /// `versionCode`, Apple's `CFBundleVersion`. `None` when unknown.
33 ///
34 /// This is a string because Apple build versions may contain multiple
35 /// numeric components, such as `42.3.1`. Android version codes are
36 /// converted without losing their numeric value.
37 fn build_version(&self) -> Option<String>;
38}
39
40pub type AppInfoRef = Rc<dyn AppInfo>;
41
42struct DefaultAppInfo;
43
44impl AppInfo for DefaultAppInfo {
45 fn version_name(&self) -> Option<String> {
46 None
47 }
48
49 fn build_version(&self) -> Option<String> {
50 None
51 }
52}
53
54thread_local! {
55 static PLATFORM_APP_INFO: RefCell<Option<AppInfoRef>> = const { RefCell::new(None) };
56}
57
58/// Installs a platform app-info implementation, replacing any previous one.
59pub fn set_platform_app_info(info: AppInfoRef) {
60 PLATFORM_APP_INFO.with(|cell| *cell.borrow_mut() = Some(info));
61}
62
63/// Removes any registered platform app info (tests and teardown).
64pub fn clear_platform_app_info() {
65 PLATFORM_APP_INFO.with(|cell| *cell.borrow_mut() = None);
66}
67
68/// The active app info: the platform implementation if installed, otherwise
69/// the built-in default.
70pub fn app_info() -> AppInfoRef {
71 PLATFORM_APP_INFO
72 .with(|cell| cell.borrow().clone())
73 .unwrap_or_else(|| Rc::new(DefaultAppInfo))
74}
75
76/// The version a user is shown, if the platform knows one.
77pub fn version_name() -> Option<String> {
78 app_info().version_name()
79}
80
81/// The build identifier a store orders releases by, if the platform knows one.
82pub fn build_version() -> Option<String> {
83 app_info().build_version()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 struct Packaged;
91
92 impl AppInfo for Packaged {
93 fn version_name(&self) -> Option<String> {
94 Some("1.4.2-debug".to_string())
95 }
96
97 fn build_version(&self) -> Option<String> {
98 Some("17.2.1".to_string())
99 }
100 }
101
102 #[test]
103 fn an_unpackaged_binary_has_no_version_to_report() {
104 clear_platform_app_info();
105 assert_eq!(version_name(), None);
106 assert_eq!(build_version(), None);
107 }
108
109 #[test]
110 fn the_platform_answer_wins_and_carries_what_packaging_added() {
111 clear_platform_app_info();
112 set_platform_app_info(Rc::new(Packaged));
113 assert_eq!(version_name().as_deref(), Some("1.4.2-debug"));
114 assert_eq!(build_version().as_deref(), Some("17.2.1"));
115 clear_platform_app_info();
116 assert_eq!(version_name(), None);
117 }
118}