mecomp_core/
lib.rs

1#![deny(clippy::missing_inline_in_public_items)]
2
3use errors::DirectoryError;
4
5#[cfg(feature = "audio")]
6pub mod audio;
7pub mod config;
8pub mod errors;
9pub mod logger;
10#[cfg(feature = "rpc")]
11pub mod rpc;
12pub mod state;
13#[cfg(any(test, feature = "test_utils"))]
14pub mod test_utils;
15#[cfg(feature = "rpc")]
16pub mod udp;
17
18#[cfg(test)]
19extern crate rstest_reuse;
20
21/// This macro returns the name of the enclosing function.
22/// As the internal implementation is based on the [`std::any::type_name`], this macro derives
23/// all the limitations of this function.
24///
25/// ## Examples
26///
27/// ```rust
28/// mod bar {
29///     pub fn sample_function() {
30///         use mecomp_core::function_name;
31///         assert!(function_name!().ends_with("bar::sample_function"));
32///     }
33/// }
34///
35/// bar::sample_function();
36/// ```
37///
38/// [`std::any::type_name`]: https://doc.rust-lang.org/std/any/fn.type_name.html
39///
40/// # Note
41///
42/// This macro is copied from the `stdext` crate. <https://github.com/popzxc/stdext-rs>
43#[macro_export]
44macro_rules! function_name {
45    () => {{
46        // Okay, this is ugly, I get it. However, this is the best we can get on a stable rust.
47        const fn f() {}
48        fn type_name_of<T>(_: T) -> &'static str {
49            std::any::type_name::<T>()
50        }
51        let name = type_name_of(f);
52        // `3` is the length of the `::f`.
53        &name[..name.len() - 3]
54    }};
55}
56
57#[must_use]
58#[inline]
59pub fn format_duration(duration: &std::time::Duration) -> String {
60    let total_seconds = duration.as_secs();
61    let hours = total_seconds / 3600;
62    let minutes = (total_seconds % 3600) / 60;
63    let seconds = duration.as_secs_f32() % 60.;
64
65    format!("{hours:02}:{minutes:02}:{seconds:05.2}")
66}
67
68/// Get the data directory for the application.
69///
70/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
71/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
72///
73/// # Errors
74///
75/// This function will return an error if the data directory could not be found.
76#[inline]
77pub fn get_data_dir() -> Result<std::path::PathBuf, DirectoryError> {
78    let directory = if let Ok(s) = std::env::var("MECOMP_DATA") {
79        std::path::PathBuf::from(s)
80    } else if let Some(proj_dirs) =
81        directories::ProjectDirs::from("com", "anthonymichaeltdm", "mecomp")
82    {
83        proj_dirs.data_local_dir().to_path_buf()
84    } else {
85        return Err(DirectoryError::Data);
86    };
87    Ok(directory)
88}
89
90/// Get the config directory for the application.
91///
92/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
93/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
94///
95/// # Errors
96///
97/// This function will return an error if the config directory could not be found.
98#[inline]
99pub fn get_config_dir() -> Result<std::path::PathBuf, DirectoryError> {
100    let directory = if let Ok(s) = std::env::var("MECOMP_CONFIG") {
101        std::path::PathBuf::from(s)
102    } else if let Some(proj_dirs) =
103        directories::ProjectDirs::from("com", "anthonymichaeltdm", "mecomp")
104    {
105        proj_dirs.config_local_dir().to_path_buf()
106    } else {
107        return Err(DirectoryError::Config);
108    };
109    Ok(directory)
110}
111
112/// Check if a server is already running on localhost on the given port.
113/// If a server is already running, return true, otherwise return false.
114#[must_use]
115#[inline]
116pub fn is_server_running(port: u16) -> bool {
117    std::net::TcpStream::connect(format!("localhost:{port}")).is_ok()
118}
119
120/// A `OnceLock` that returns a default value if it has not been set yet.
121#[derive(Debug, Clone)]
122pub struct OnceLockDefault<T> {
123    value: std::sync::OnceLock<T>,
124    default: T,
125}
126
127impl<T> OnceLockDefault<T> {
128    /// Creates a new `OnceLockDefault` with the given default value.
129    #[inline]
130    pub const fn new(default: T) -> Self {
131        Self {
132            value: std::sync::OnceLock::new(),
133            default,
134        }
135    }
136
137    /// Initializes the contents of the cell to value.
138    ///
139    /// May block if another thread is currently attempting to initialize the cell.
140    /// The cell is guaranteed to contain a value when set returns, though not necessarily the one provided.
141    ///
142    /// # Errors
143    ///
144    /// Returns `Ok(())` if the cell was uninitialized and `Err(value)` if the cell was already initialized.
145    #[inline]
146    pub fn set(&self, value: T) -> Result<(), T> {
147        self.value.set(value)
148    }
149
150    /// Gets the reference to the underlying value, if set. Otherwise returns a reference to the default value.
151    ///
152    /// This method never blocks.
153    #[inline]
154    pub fn get(&self) -> &T {
155        self.value.get().unwrap_or(&self.default)
156    }
157
158    /// Checks if the cell has been initialized.
159    ///
160    /// This method never blocks.
161    #[inline]
162    pub fn is_initialized(&self) -> bool {
163        self.value.get().is_some()
164    }
165}
166
167impl<T> std::ops::Deref for OnceLockDefault<T> {
168    type Target = T;
169
170    #[inline]
171    fn deref(&self) -> &Self::Target {
172        self.get()
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::format_duration;
179    use pretty_assertions::assert_eq;
180    use rstest::rstest;
181    use std::time::Duration;
182
183    #[rstest]
184    #[case::zero(Duration::from_secs(0), "00:00:00.00")]
185    #[case::sub_second(Duration::from_millis(100), "00:00:00.10")]
186    #[case::sub_second(Duration::from_millis(101), "00:00:00.10")]
187    #[case::one_second(Duration::from_secs(1), "00:00:01.00")]
188    #[case::one_minute(Duration::from_secs(60), "00:01:00.00")]
189    #[case::one_hour(Duration::from_secs(3600), "01:00:00.00")]
190    #[case::one_hour_one_minute_one_second(Duration::from_secs(3661), "01:01:01.00")]
191    #[case(Duration::from_secs(3600 + 120 + 1), "01:02:01.00")]
192    fn test_format_duration(#[case] duration: Duration, #[case] expected: &str) {
193        let actual = format_duration(&duration);
194        assert_eq!(actual, expected);
195    }
196
197    #[test]
198    fn test_function_name() {
199        fn test_function() {
200            let result = super::function_name!();
201            assert!(result.ends_with("test_function"));
202        }
203
204        test_function();
205    }
206
207    #[test]
208    fn test_get_data_dir() {
209        let data_dir = super::get_data_dir().unwrap();
210        assert_eq!(
211            data_dir
212                .components()
213                .next_back()
214                .unwrap()
215                .as_os_str()
216                .to_string_lossy(),
217            "mecomp"
218        );
219    }
220
221    #[test]
222    fn test_get_config_dir() {
223        let config_dir = super::get_config_dir().unwrap();
224        assert_eq!(
225            config_dir
226                .components()
227                .next_back()
228                .unwrap()
229                .as_os_str()
230                .to_string_lossy(),
231            "mecomp"
232        );
233    }
234}