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}