mcvm_shared/util/
mod.rs

1/// Printing and output utilities
2pub mod print;
3
4use std::process::{Command, Stdio};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use cfg_match::cfg_match;
8
9macro_rules! def_matched_item {
10	($cfg:ident, $doc:literal, $name:ident, $err:literal, $($k:literal: $v: literal);* $(;)?) => {
11		cfg_match! {
12			$(
13				$cfg = $k => {
14					#[doc = $doc]
15					pub const $name: &str = $v;
16				}
17			)*
18			_ => {
19				compile_error!($err)
20				pub const $name: &str = "";
21			}
22		}
23	};
24}
25
26def_matched_item! {
27	target_os,
28	"String representing the current operating system",
29	OS_STRING,
30	"Target operating system is unsupported",
31	"linux": "linux";
32	"windows": "windows";
33	"macos": "macos";
34	"ios": "ios";
35	"android": "android";
36	"freebsd": "freebsd";
37	"dragonfly": "dragonfly";
38	"bitrig": "bitrig";
39	"netbsd": "netbsd";
40	"openbsd": "openbsd";
41}
42
43def_matched_item! {
44	target_arch,
45	"String representing the current architecture",
46	ARCH_STRING,
47	"Target architecture is unsupported",
48	"x86": "x86";
49	"x86_64": "x86_64";
50	"arm": "arm";
51	"aarch64": "aarch64";
52	"riscv32": "riscv32";
53	"riscv64": "riscv64";
54	"mips": "mips";
55	"mips64": "mips64";
56	"powerpc": "powerpc";
57	"powerpc64": "powerpc64";
58}
59
60cfg_match! {
61	target_os = "linux" => {
62		/// String of the preferred archive file extension
63		pub const PREFERRED_ARCHIVE: &str = "tar.gz";
64	}
65	_ => {
66		/// String of the preferred archive file extension
67		pub const PREFERRED_ARCHIVE: &str = "zip";
68	}
69}
70
71/// Adds a dot to the preferred archive name
72pub fn preferred_archive_extension() -> String {
73	format!(".{PREFERRED_ARCHIVE}")
74}
75
76cfg_match! {
77	target_pointer_width = "64" => {
78		/// String representing the current pointer width
79		pub const TARGET_BITS_STR: &str = "64";
80	}
81	_ => {
82		/// String representing the current pointer width
83		pub const TARGET_BITS_STR: &str = "32";
84	}
85}
86
87/// Skip in a loop if a result fails
88#[macro_export]
89macro_rules! skip_fail {
90	($res:expr) => {
91		match $res {
92			Ok(val) => val,
93			Err(..) => continue,
94		}
95	};
96}
97
98/// Skip in a loop if an option is none
99#[macro_export]
100macro_rules! skip_none {
101	($res:expr) => {
102		match $res {
103			Some(val) => val,
104			None => continue,
105		}
106	};
107}
108
109/// Capitalizes the first character of a string
110pub fn cap_first_letter(string: &str) -> String {
111	let mut c = string.chars();
112	match c.next() {
113		None => String::new(),
114		Some(f) => f.to_uppercase().chain(c).collect(),
115	}
116}
117
118/// Merges two options together with the right one taking precedence
119///
120/// Right takes precedence when they are both some
121/// ```
122/// use mcvm_shared::util::merge_options;
123///
124/// let x = Some(7);
125/// let y = Some(8);
126/// assert_eq!(merge_options(x, y), Some(8));
127/// ```
128/// Right is some so it overwrites none
129/// ```
130/// use mcvm_shared::util::merge_options;
131///
132/// let x = None;
133/// let y = Some(12);
134/// assert_eq!(merge_options(x, y), Some(12));
135/// ```
136/// Uses left because right is none:
137/// ```
138/// use mcvm_shared::util::merge_options;
139///
140/// let x = Some(5);
141/// let y = None;
142/// assert_eq!(merge_options(x, y), Some(5));
143/// ```
144pub fn merge_options<T>(left: Option<T>, right: Option<T>) -> Option<T> {
145	if right.is_some() {
146		right
147	} else {
148		left
149	}
150}
151
152/// Gets the current UTC timestamp in seconds
153pub fn utc_timestamp() -> anyhow::Result<u64> {
154	Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
155}
156
157/// Trait for a value that can be converted to an integer
158pub trait ToInt {
159	/// Get this value as an i32
160	fn to_int(&self) -> i32;
161}
162
163impl ToInt for bool {
164	fn to_int(&self) -> i32 {
165		*self as i32
166	}
167}
168
169// Command for opening links
170cfg_match! {
171	target_os = "linux" => {
172		const URL_OPEN_CMD: Option<&str> = Some("xdg-open");
173	}
174	target_os = "windows" => {
175		const URL_OPEN_CMD: Option<&str> = Some("start");
176	}
177	target_os = "macos" => {
178		const URL_OPEN_CMD: Option<&str> = Some("open");
179	}
180	_ => {
181		const URL_OPEN_CMD: Option<&str> = None;
182	}
183}
184
185/// Attempt to open a link on the user's computer
186pub fn open_link(link: &str) -> anyhow::Result<()> {
187	let Some(cmd) = URL_OPEN_CMD else {
188		return Ok(());
189	};
190
191	Command::new(cmd)
192		.arg(link)
193		.stderr(Stdio::null())
194		.stdout(Stdio::null())
195		.spawn()?;
196
197	Ok(())
198}
199
200#[cfg(feature = "schema")]
201use schemars::JsonSchema;
202use serde::{Deserialize, Serialize};
203
204/// Converts "yes" or "no" to a boolean
205pub fn yes_no(string: &str) -> Option<bool> {
206	match string {
207		"yes" => Some(true),
208		"no" => Some(false),
209		_ => None,
210	}
211}
212
213/// Checks if a string is a valid identifier
214pub fn is_valid_identifier(id: &str) -> bool {
215	for c in id.chars() {
216		if !c.is_ascii() {
217			return false;
218		}
219
220		if c.is_ascii_punctuation() {
221			match c {
222				'_' | '-' | '.' => {}
223				_ => return false,
224			}
225		}
226
227		if c.is_ascii_whitespace() {
228			return false;
229		}
230	}
231
232	true
233}
234
235/// Utility enum for deserialization that lets you do a list that can be one item
236/// without the braces
237#[derive(Deserialize, Debug, Clone, Eq)]
238#[cfg_attr(feature = "schema", derive(JsonSchema))]
239#[serde(untagged)]
240pub enum DeserListOrSingle<T> {
241	/// Only one item, specified without braces
242	Single(T),
243	/// A list of items, specified with braces
244	List(Vec<T>),
245}
246
247impl<T> Default for DeserListOrSingle<T> {
248	fn default() -> Self {
249		Self::List(Vec::default())
250	}
251}
252
253impl<T: Serialize> Serialize for DeserListOrSingle<T> {
254	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255	where
256		S: serde::Serializer,
257	{
258		match self {
259			Self::List(list) => {
260				if list.len() == 1 {
261					list[0].serialize(serializer)
262				} else {
263					list.serialize(serializer)
264				}
265			}
266			Self::Single(val) => val.serialize(serializer),
267		}
268	}
269}
270
271impl<T> DeserListOrSingle<T> {
272	/// Checks if this value is empty
273	pub fn is_empty(&self) -> bool {
274		matches!(self, Self::List(list) if list.is_empty())
275	}
276
277	/// Checks if an option of this struct is empty
278	pub fn is_option_empty(val: &Option<Self>) -> bool {
279		val.is_none() || matches!(val, Some(val) if val.is_empty())
280	}
281
282	/// Iterates over this DeserListOrSingle
283	pub fn iter(&self) -> DeserListOrSingleIter<'_, T> {
284		match &self {
285			Self::Single(val) => {
286				DeserListOrSingleIter(DeserListOrSingleIterState::Single(Some(val)))
287			}
288			Self::List(list) => {
289				DeserListOrSingleIter(DeserListOrSingleIterState::List(list.iter()))
290			}
291		}
292	}
293}
294
295impl<T: Clone> DeserListOrSingle<T> {
296	/// Get the contained value as a Vec
297	pub fn get_vec(&self) -> Vec<T> {
298		match &self {
299			Self::Single(val) => vec![val.clone()],
300			Self::List(list) => list.clone(),
301		}
302	}
303
304	/// Merges this enum with another
305	pub fn merge(&mut self, other: Self) {
306		let mut self_vec = self.get_vec();
307		self_vec.extend(other.iter().cloned());
308		*self = Self::List(self_vec);
309	}
310}
311
312impl<T: PartialEq> PartialEq for DeserListOrSingle<T> {
313	fn eq(&self, other: &Self) -> bool {
314		match (self, other) {
315			(DeserListOrSingle::Single(l), DeserListOrSingle::Single(r)) => l == r,
316			(DeserListOrSingle::List(l), DeserListOrSingle::List(r)) => l == r,
317			(DeserListOrSingle::List(l), DeserListOrSingle::Single(r)) => {
318				l.len() == 1 && l.first().expect("Length is 1") == r
319			}
320			(DeserListOrSingle::Single(l), DeserListOrSingle::List(r)) => {
321				r.len() == 1 && r.first().expect("Length is 1") == l
322			}
323		}
324	}
325}
326
327impl<T: Clone> Extend<T> for DeserListOrSingle<T> {
328	fn extend<U: IntoIterator<Item = T>>(&mut self, iter: U) {
329		// Convert single to list
330		if let Self::Single(item) = self {
331			*self = Self::List(vec![item.clone()]);
332		}
333		// Extend the list
334		if let Self::List(list) = self {
335			list.extend(iter);
336			// Convert back to single if there is only one item
337			if list.len() == 1 {
338				*self = Self::Single(list.first().expect("Length is 1").clone());
339			}
340		}
341	}
342}
343
344/// Iterator over DeserListOrSingle
345pub struct DeserListOrSingleIter<'a, T>(DeserListOrSingleIterState<'a, T>);
346
347/// State for a DeserListOrSingleIter
348enum DeserListOrSingleIterState<'a, T> {
349	Single(Option<&'a T>),
350	List(std::slice::Iter<'a, T>),
351}
352
353impl<'a, T> Iterator for DeserListOrSingleIter<'a, T> {
354	type Item = &'a T;
355
356	fn next(&mut self) -> Option<Self::Item> {
357		match &mut self.0 {
358			DeserListOrSingleIterState::Single(val) => val.take(),
359			DeserListOrSingleIterState::List(slice_iter) => slice_iter.next(),
360		}
361	}
362}
363
364/// Extension trait for Default
365pub trait DefaultExt {
366	/// Check if the value is equal to it's default value
367	fn is_default(&self) -> bool;
368}
369
370impl<T: Default + PartialEq> DefaultExt for T {
371	fn is_default(&self) -> bool {
372		self == &Self::default()
373	}
374}
375
376/// Macro to try a fallible operation multiple times before giving up and returning an error
377#[macro_export]
378macro_rules! try_3 {
379	($op:block) => {
380		if let Ok(out) = $op {
381			Ok(out)
382		} else {
383			if let Ok(out) = $op {
384				Ok(out)
385			} else {
386				$op
387			}
388		}
389	};
390}
391
392#[cfg(test)]
393mod tests {
394	use super::*;
395
396	#[test]
397	fn test_id_validation() {
398		assert!(is_valid_identifier("hello"));
399		assert!(is_valid_identifier("Hello"));
400		assert!(is_valid_identifier("H3110"));
401		assert!(is_valid_identifier("hello-world"));
402		assert!(is_valid_identifier("hello_world"));
403		assert!(is_valid_identifier("hello.world"));
404		assert!(!is_valid_identifier("hello*world"));
405		assert!(!is_valid_identifier("hello\nworld"));
406		assert!(!is_valid_identifier("hello world"));
407	}
408
409	#[test]
410	fn test_deser_list_or_single_iter() {
411		let item = DeserListOrSingle::Single(7);
412		assert_eq!(item.iter().next(), Some(&7));
413
414		let item = DeserListOrSingle::List(vec![1, 2, 3]);
415		let mut iter = item.iter();
416		assert_eq!(iter.next(), Some(&1));
417		assert_eq!(iter.next(), Some(&2));
418		assert_eq!(iter.next(), Some(&3));
419	}
420}