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}