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()
16//! .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
17//! ```
18//!
19//! The default implementation answers `None` for both: a desktop binary run
20//! straight out of `target/` was not packaged by anything and has no answer to
21//! give. Platform backends install a real one with
22//! [`set_platform_app_info`].
23
24use std::cell::RefCell;
25use std::rc::Rc;
26
27/// Reports the packaged identity of the running app.
28pub trait AppInfo {
29 /// The version a user is shown — Android's `versionName`, Apple's
30 /// `CFBundleShortVersionString`. `None` when the platform has none.
31 fn version_name(&self) -> Option<String>;
32
33 /// The build identifier a store orders releases by — Android's
34 /// `versionCode`, Apple's `CFBundleVersion`. `None` when unknown.
35 ///
36 /// This is a string because Apple build versions may contain multiple
37 /// numeric components, such as `42.3.1`. Android version codes are
38 /// converted without losing their numeric value.
39 fn build_version(&self) -> Option<String>;
40}
41
42pub type AppInfoRef = Rc<dyn AppInfo>;
43
44struct DefaultAppInfo;
45
46impl AppInfo for DefaultAppInfo {
47 fn version_name(&self) -> Option<String> {
48 None
49 }
50
51 fn build_version(&self) -> Option<String> {
52 None
53 }
54}
55
56thread_local! {
57 static PLATFORM_APP_INFO: RefCell<Option<AppInfoRef>> = const { RefCell::new(None) };
58}
59
60/// Installs a platform app-info implementation, replacing any previous one.
61pub fn set_platform_app_info(info: AppInfoRef) {
62 PLATFORM_APP_INFO.with(|cell| *cell.borrow_mut() = Some(info));
63}
64
65/// Removes any registered platform app info (tests and teardown).
66pub fn clear_platform_app_info() {
67 PLATFORM_APP_INFO.with(|cell| *cell.borrow_mut() = None);
68}
69
70/// The active app info: the platform implementation if installed, otherwise
71/// the built-in default.
72pub fn app_info() -> AppInfoRef {
73 PLATFORM_APP_INFO
74 .with(|cell| cell.borrow().clone())
75 .unwrap_or_else(|| Rc::new(DefaultAppInfo))
76}
77
78/// The version a user is shown, if the platform knows one.
79pub fn version_name() -> Option<String> {
80 app_info().version_name()
81}
82
83/// The build identifier a store orders releases by, if the platform knows one.
84pub fn build_version() -> Option<String> {
85 app_info().build_version()
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 struct Packaged;
93
94 impl AppInfo for Packaged {
95 fn version_name(&self) -> Option<String> {
96 Some("1.4.2-debug".to_string())
97 }
98
99 fn build_version(&self) -> Option<String> {
100 Some("17.2.1".to_string())
101 }
102 }
103
104 #[test]
105 fn an_unpackaged_binary_has_no_version_to_report() {
106 clear_platform_app_info();
107 assert_eq!(version_name(), None);
108 assert_eq!(build_version(), None);
109 }
110
111 #[test]
112 fn the_platform_answer_wins_and_carries_what_packaging_added() {
113 clear_platform_app_info();
114 set_platform_app_info(Rc::new(Packaged));
115 // The suffix is the whole point: a compile-time constant cannot know
116 // about it, because the packaging step is what adds it.
117 assert_eq!(version_name().as_deref(), Some("1.4.2-debug"));
118 assert_eq!(build_version().as_deref(), Some("17.2.1"));
119 clear_platform_app_info();
120 assert_eq!(version_name(), None);
121 }
122}