mecomp_core/
lib.rs

1use errors::DirectoryError;
2
3#[cfg(feature = "audio")]
4pub mod audio;
5pub mod errors;
6pub mod logger;
7#[cfg(feature = "rpc")]
8pub mod rpc;
9pub mod state;
10#[cfg(any(test, feature = "test_utils"))]
11pub mod test_utils;
12#[cfg(feature = "rpc")]
13pub mod udp;
14
15#[cfg(test)]
16extern crate rstest_reuse;
17
18/// This macro returns the name of the enclosing function.
19/// As the internal implementation is based on the [`std::any::type_name`], this macro derives
20/// all the limitations of this function.
21///
22/// ## Examples
23///
24/// ```rust
25/// mod bar {
26///     pub fn sample_function() {
27///         use mecomp_core::function_name;
28///         assert!(function_name!().ends_with("bar::sample_function"));
29///     }
30/// }
31///
32/// bar::sample_function();
33/// ```
34///
35/// [`std::any::type_name`]: https://doc.rust-lang.org/std/any/fn.type_name.html
36///
37/// # Note
38///
39/// This macro is copied from the `stdext` crate. <https://github.com/popzxc/stdext-rs>
40#[macro_export]
41macro_rules! function_name {
42    () => {{
43        // Okay, this is ugly, I get it. However, this is the best we can get on a stable rust.
44        const fn f() {}
45        fn type_name_of<T>(_: T) -> &'static str {
46            std::any::type_name::<T>()
47        }
48        let name = type_name_of(f);
49        // `3` is the length of the `::f`.
50        &name[..name.len() - 3]
51    }};
52}
53
54#[must_use]
55pub fn format_duration(duration: &std::time::Duration) -> String {
56    let total_seconds = duration.as_secs();
57    let hours = total_seconds / 3600;
58    let minutes = (total_seconds % 3600) / 60;
59    let seconds = duration.as_secs_f32() % 60.;
60
61    format!("{hours:02}:{minutes:02}:{seconds:05.2}")
62}
63
64/// Get the data directory for the application.
65///
66/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
67/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
68///
69/// # Errors
70///
71/// This function will return an error if the data directory could not be found.
72pub fn get_data_dir() -> Result<std::path::PathBuf, DirectoryError> {
73    let directory = if let Ok(s) = std::env::var("MECOMP_DATA") {
74        std::path::PathBuf::from(s)
75    } else if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "mecomp") {
76        proj_dirs.data_local_dir().to_path_buf()
77    } else {
78        return Err(DirectoryError::Data);
79    };
80    Ok(directory)
81}
82
83/// Get the config directory for the application.
84///
85/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
86/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
87///
88/// # Errors
89///
90/// This function will return an error if the config directory could not be found.
91pub fn get_config_dir() -> Result<std::path::PathBuf, DirectoryError> {
92    let directory = if let Ok(s) = std::env::var("MECOMP_CONFIG") {
93        std::path::PathBuf::from(s)
94    } else if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "mecomp") {
95        proj_dirs.config_local_dir().to_path_buf()
96    } else {
97        return Err(DirectoryError::Config);
98    };
99    Ok(directory)
100}
101
102/// Check if a server is already running on localhost on the given port.
103/// If a server is already running, return true, otherwise return false.
104#[must_use]
105pub fn is_server_running(port: u16) -> bool {
106    std::net::TcpStream::connect(format!("localhost:{port}")).is_ok()
107}
108
109#[cfg(test)]
110mod test {
111    use super::format_duration;
112    use pretty_assertions::assert_eq;
113    use rstest::rstest;
114    use std::time::Duration;
115
116    #[rstest]
117    #[case::zero(Duration::from_secs(0), "00:00:00.00")]
118    #[case::sub_second(Duration::from_millis(100), "00:00:00.10")]
119    #[case::sub_second(Duration::from_millis(101), "00:00:00.10")]
120    #[case::one_second(Duration::from_secs(1), "00:00:01.00")]
121    #[case::one_minute(Duration::from_secs(60), "00:01:00.00")]
122    #[case::one_hour(Duration::from_secs(3600), "01:00:00.00")]
123    #[case::one_hour_one_minute_one_second(Duration::from_secs(3661), "01:01:01.00")]
124    #[case(Duration::from_secs(3600 + 120 + 1), "01:02:01.00")]
125    fn test_format_duration(#[case] duration: Duration, #[case] expected: &str) {
126        let actual = format_duration(&duration);
127        assert_eq!(actual, expected);
128    }
129
130    #[test]
131    fn test_function_name() {
132        fn test_function() {
133            let result = super::function_name!();
134            assert!(result.ends_with("test_function"));
135        }
136
137        test_function();
138    }
139
140    #[test]
141    fn test_get_data_dir() {
142        let data_dir = super::get_data_dir().unwrap();
143        assert_eq!(
144            data_dir
145                .components()
146                .last()
147                .unwrap()
148                .as_os_str()
149                .to_string_lossy(),
150            "mecomp"
151        );
152    }
153
154    #[test]
155    fn test_get_config_dir() {
156        let config_dir = super::get_config_dir().unwrap();
157        assert_eq!(
158            config_dir
159                .components()
160                .last()
161                .unwrap()
162                .as_os_str()
163                .to_string_lossy(),
164            "mecomp"
165        );
166    }
167}