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_host(host: &compact_str::CompactString, _context: &()) -> Result<(), garde::Error> {
54    if host.parse::<std::net::IpAddr>().is_ok() {
55        return Ok(());
56    }
57
58    let is_valid_hostname = host.len() <= 253
59        && host.split('.').all(|label| {
60            !label.is_empty()
61                && label.len() <= 63
62                && !label.starts_with('-')
63                && !label.ends_with('-')
64                && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
65        });
66
67    if !is_valid_hostname {
68        return Err(garde::Error::new("must be a valid IP address or hostname"));
69    }
70
71    Ok(())
72}
73
74pub fn validate_http_url(url: &reqwest::Url, _context: &()) -> Result<(), garde::Error> {
75    if !matches!(url.scheme(), "http" | "https") {
76        return Err(garde::Error::new("must be a http or https url"));
77    }
78
79    Ok(())
80}
81
82pub fn validate_json_path(path: &str, _context: &()) -> Result<(), garde::Error> {
83    if let Err(err) = serde_json_path::JsonPath::parse(path) {
84        return Err(garde::Error::new(compact_str::format_compact!(
85            "must be a valid json path: {err}"
86        )));
87    }
88
89    Ok(())
90}
91
92pub fn validate_time_in_future(
93    time: &chrono::DateTime<chrono::Utc>,
94    _context: &(),
95) -> Result<(), garde::Error> {
96    let now = chrono::Utc::now();
97    if *time <= now {
98        return Err(garde::Error::new("time must be in the future"));
99    }
100
101    Ok(())
102}
103
104#[inline]
105pub fn validate_data<T: Validate>(data: &T) -> Result<(), Vec<String>>
106where
107    T::Context: Default,
108{
109    if let Err(err) = data.validate() {
110        let error_messages = flatten_validation_errors(&err);
111
112        return Err(error_messages);
113    }
114
115    Ok(())
116}
117
118pub fn flatten_validation_errors(errors: &garde::Report) -> Vec<String> {
119    let mut messages = Vec::new();
120
121    for (path, error) in errors.iter() {
122        let full_name = path.to_compact_string();
123
124        messages.push(format!("{full_name}: {}", error.message()));
125    }
126
127    messages
128}
129
130pub fn axum_to_tungstenite(
131    msg: axum::extract::ws::Message,
132) -> tokio_tungstenite::tungstenite::Message {
133    use axum::extract::ws::Message;
134    use tokio_tungstenite::tungstenite::{Message as Tung, protocol::CloseFrame as TungClose};
135
136    match msg {
137        Message::Text(text) => Tung::Text(text.as_str().into()),
138        Message::Binary(data) => Tung::Binary(data),
139        Message::Ping(data) => Tung::Ping(data),
140        Message::Pong(data) => Tung::Pong(data),
141        Message::Close(frame) => Tung::Close(frame.map(|f| TungClose {
142            code: f.code.into(),
143            reason: f.reason.as_str().into(),
144        })),
145    }
146}
147
148pub fn tungstenite_to_axum(
149    msg: tokio_tungstenite::tungstenite::Message,
150) -> Option<axum::extract::ws::Message> {
151    use axum::extract::ws::{CloseFrame, Message};
152    use tokio_tungstenite::tungstenite::Message as Tung;
153
154    Some(match msg {
155        Tung::Text(text) => Message::Text(text.as_str().into()),
156        Tung::Binary(data) => Message::Binary(data),
157        Tung::Ping(data) => Message::Ping(data),
158        Tung::Pong(data) => Message::Pong(data),
159        Tung::Close(frame) => Message::Close(frame.map(|f| CloseFrame {
160            code: f.code.into(),
161            reason: f.reason.as_str().into(),
162        })),
163        Tung::Frame(_) => return None,
164    })
165}
166
167pub fn api_key_scope(auth: Option<&GetAuthMethod>) -> Option<&[compact_str::CompactString]> {
168    match &***auth? {
169        AuthMethod::ApiKey(api_key) => Some(&api_key.server_permissions),
170        _ => None,
171    }
172}
173
174pub fn push_scope_or_star<'a>(
175    permissions: &mut Vec<&'a str>,
176    scope: Option<&'a [compact_str::CompactString]>,
177) {
178    match scope {
179        Some(scope) => permissions.extend(scope.iter().map(compact_str::CompactString::as_str)),
180        None => permissions.push("*"),
181    }
182}