1pub 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 pub const PREFERRED_ARCHIVE: &str = "tar.gz";
64 }
65 _ => {
66 pub const PREFERRED_ARCHIVE: &str = "zip";
68 }
69}
70
71pub fn preferred_archive_extension() -> String {
73 format!(".{PREFERRED_ARCHIVE}")
74}
75
76cfg_match! {
77 target_pointer_width = "64" => {
78 pub const TARGET_BITS_STR: &str = "64";
80 }
81 _ => {
82 pub const TARGET_BITS_STR: &str = "32";
84 }
85}
86
87#[macro_export]
89macro_rules! skip_fail {
90 ($res:expr) => {
91 match $res {
92 Ok(val) => val,
93 Err(..) => continue,
94 }
95 };
96}
97
98#[macro_export]
100macro_rules! skip_none {
101 ($res:expr) => {
102 match $res {
103 Some(val) => val,
104 None => continue,
105 }
106 };
107}
108
109pub 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
118pub 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
152pub fn utc_timestamp() -> anyhow::Result<u64> {
154 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
155}
156
157pub trait ToInt {
159 fn to_int(&self) -> i32;
161}
162
163impl ToInt for bool {
164 fn to_int(&self) -> i32 {
165 *self as i32
166 }
167}
168
169cfg_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
185pub 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
204pub fn yes_no(string: &str) -> Option<bool> {
206 match string {
207 "yes" => Some(true),
208 "no" => Some(false),
209 _ => None,
210 }
211}
212
213pub 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#[derive(Deserialize, Debug, Clone, Eq)]
238#[cfg_attr(feature = "schema", derive(JsonSchema))]
239#[serde(untagged)]
240pub enum DeserListOrSingle<T> {
241 Single(T),
243 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 pub fn is_empty(&self) -> bool {
274 matches!(self, Self::List(list) if list.is_empty())
275 }
276
277 pub fn is_option_empty(val: &Option<Self>) -> bool {
279 val.is_none() || matches!(val, Some(val) if val.is_empty())
280 }
281
282 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 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 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 if let Self::Single(item) = self {
331 *self = Self::List(vec![item.clone()]);
332 }
333 if let Self::List(list) = self {
335 list.extend(iter);
336 if list.len() == 1 {
338 *self = Self::Single(list.first().expect("Length is 1").clone());
339 }
340 }
341 }
342}
343
344pub struct DeserListOrSingleIter<'a, T>(DeserListOrSingleIterState<'a, T>);
346
347enum 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
364pub trait DefaultExt {
366 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_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}