Skip to main content

shared/
utils.rs

1use crate::models::user::{AuthMethod, GetAuthMethod};
2use colored::Colorize;
3use compact_str::ToCompactString;
4use garde::Validate;
5
6pub fn handle_startup_error<T>(err: anyhow::Error) -> T {
7    eprintln!("{}: {err:#?}", "an error occurred during startup".red());
8    std::process::exit(1);
9}
10
11#[inline]
12pub fn slice_up_to(s: &str, max_len: usize) -> &str {
13    if max_len >= s.len() || s.is_empty() {
14        return s;
15    }
16
17    let mut idx = max_len;
18    while !s.is_char_boundary(idx) {
19        idx -= 1;
20    }
21
22    &s[..idx]
23}
24
25#[inline]
26pub fn truncate_up_to(mut s: String, max_len: usize) -> String {
27    if max_len >= s.len() || s.is_empty() {
28        return s;
29    }
30
31    let mut idx = max_len;
32    while !s.is_char_boundary(idx) {
33        idx -= 1;
34    }
35
36    s.truncate(idx);
37    s
38}
39
40pub fn validate_language(
41    language: &compact_str::CompactString,
42    _context: &(),
43) -> Result<(), garde::Error> {
44    if !crate::FRONTEND_LANGUAGES.contains(language) {
45        return Err(garde::Error::new(compact_str::format_compact!(
46            "invalid language: {language}"
47        )));
48    }
49
50    Ok(())
51}
52
53pub fn validate_time_in_future(
54    time: &chrono::DateTime<chrono::Utc>,
55    _context: &(),
56) -> Result<(), garde::Error> {
57    let now = chrono::Utc::now();
58    if *time <= now {
59        return Err(garde::Error::new("time must be in the future"));
60    }
61
62    Ok(())
63}
64
65#[inline]
66pub fn validate_data<T: Validate>(data: &T) -> Result<(), Vec<String>>
67where
68    T::Context: Default,
69{
70    if let Err(err) = data.validate() {
71        let error_messages = flatten_validation_errors(&err);
72
73        return Err(error_messages);
74    }
75
76    Ok(())
77}
78
79pub fn flatten_validation_errors(errors: &garde::Report) -> Vec<String> {
80    let mut messages = Vec::new();
81
82    for (path, error) in errors.iter() {
83        let full_name = path.to_compact_string();
84
85        messages.push(format!("{full_name}: {}", error.message()));
86    }
87
88    messages
89}
90
91pub fn axum_to_tungstenite(
92    msg: axum::extract::ws::Message,
93) -> tokio_tungstenite::tungstenite::Message {
94    use axum::extract::ws::Message;
95    use tokio_tungstenite::tungstenite::{Message as Tung, protocol::CloseFrame as TungClose};
96
97    match msg {
98        Message::Text(text) => Tung::Text(text.as_str().into()),
99        Message::Binary(data) => Tung::Binary(data),
100        Message::Ping(data) => Tung::Ping(data),
101        Message::Pong(data) => Tung::Pong(data),
102        Message::Close(frame) => Tung::Close(frame.map(|f| TungClose {
103            code: f.code.into(),
104            reason: f.reason.as_str().into(),
105        })),
106    }
107}
108
109pub fn tungstenite_to_axum(
110    msg: tokio_tungstenite::tungstenite::Message,
111) -> Option<axum::extract::ws::Message> {
112    use axum::extract::ws::{CloseFrame, Message};
113    use tokio_tungstenite::tungstenite::Message as Tung;
114
115    Some(match msg {
116        Tung::Text(text) => Message::Text(text.as_str().into()),
117        Tung::Binary(data) => Message::Binary(data),
118        Tung::Ping(data) => Message::Ping(data),
119        Tung::Pong(data) => Message::Pong(data),
120        Tung::Close(frame) => Message::Close(frame.map(|f| CloseFrame {
121            code: f.code.into(),
122            reason: f.reason.as_str().into(),
123        })),
124        Tung::Frame(_) => return None,
125    })
126}
127
128pub fn api_key_scope(auth: Option<&GetAuthMethod>) -> Option<&[compact_str::CompactString]> {
129    match &***auth? {
130        AuthMethod::ApiKey(api_key) => Some(&api_key.server_permissions),
131        _ => None,
132    }
133}
134
135pub fn push_scope_or_star<'a>(
136    permissions: &mut Vec<&'a str>,
137    scope: Option<&'a [compact_str::CompactString]>,
138) {
139    match scope {
140        Some(scope) => permissions.extend(scope.iter().map(compact_str::CompactString::as_str)),
141        None => permissions.push("*"),
142    }
143}