2025-06-14 02:10:12 +02:00
|
|
|
use std::{fmt::Write, sync::LazyLock};
|
2025-05-15 22:47:29 +02:00
|
|
|
|
2022-02-26 21:16:11 -07:00
|
|
|
use itertools::Itertools;
|
2021-06-16 09:05:35 -07:00
|
|
|
use regex::Regex;
|
|
|
|
|
use validator::{ValidationErrors, ValidationErrorsKind};
|
|
|
|
|
|
2023-10-30 11:14:38 -05:00
|
|
|
use crate::models::pats::Scopes;
|
|
|
|
|
|
2025-05-15 22:47:29 +02:00
|
|
|
pub static RE_URL_SAFE: LazyLock<Regex> =
|
2025-05-16 23:44:03 +02:00
|
|
|
LazyLock::new(|| Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap());
|
|
|
|
|
pub static RE_USERNAME: LazyLock<Regex> =
|
|
|
|
|
LazyLock::new(|| Regex::new(r#"^[a-zA-Z0-9_-]*$"#).unwrap());
|
2021-06-16 09:05:35 -07:00
|
|
|
|
|
|
|
|
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
|
2024-10-18 16:07:35 -07:00
|
|
|
pub fn validation_errors_to_string(
|
|
|
|
|
errors: ValidationErrors,
|
|
|
|
|
adder: Option<String>,
|
|
|
|
|
) -> String {
|
2021-06-16 09:05:35 -07:00
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
|
|
let map = errors.into_errors();
|
|
|
|
|
|
2025-05-09 07:27:55 -05:00
|
|
|
let key_option = map.keys().next();
|
2021-06-16 09:05:35 -07:00
|
|
|
|
2025-08-08 15:50:44 -07:00
|
|
|
if let Some(field) = key_option
|
|
|
|
|
&& let Some(error) = map.get(field)
|
|
|
|
|
{
|
|
|
|
|
return match error {
|
|
|
|
|
ValidationErrorsKind::Struct(errors) => {
|
|
|
|
|
validation_errors_to_string(
|
|
|
|
|
*errors.clone(),
|
|
|
|
|
Some(format!("of item {field}")),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
ValidationErrorsKind::List(list) => {
|
|
|
|
|
if let Some((index, errors)) = list.iter().next() {
|
|
|
|
|
output.push_str(&validation_errors_to_string(
|
2024-10-18 16:07:35 -07:00
|
|
|
*errors.clone(),
|
2025-08-08 15:50:44 -07:00
|
|
|
Some(format!("of list {field} with index {index}")),
|
|
|
|
|
));
|
2021-06-16 09:05:35 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 15:50:44 -07:00
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
ValidationErrorsKind::Field(errors) => {
|
|
|
|
|
if let Some(error) = errors.first() {
|
|
|
|
|
if let Some(adder) = adder {
|
|
|
|
|
write!(
|
2025-06-14 02:10:12 +02:00
|
|
|
&mut output,
|
|
|
|
|
"Field {field} {adder} failed validation with error: {}",
|
|
|
|
|
error.code
|
|
|
|
|
).unwrap();
|
2025-08-08 15:50:44 -07:00
|
|
|
} else {
|
|
|
|
|
write!(
|
|
|
|
|
&mut output,
|
|
|
|
|
"Field {field} failed validation with error: {}",
|
|
|
|
|
error.code
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2021-06-16 09:05:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 15:50:44 -07:00
|
|
|
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-06-16 09:05:35 -07:00
|
|
|
}
|
|
|
|
|
|
2021-10-12 11:26:59 +08:00
|
|
|
String::new()
|
2021-06-16 09:05:35 -07:00
|
|
|
}
|
2022-02-26 21:16:11 -07:00
|
|
|
|
|
|
|
|
pub fn validate_deps(
|
|
|
|
|
values: &[crate::models::projects::Dependency],
|
|
|
|
|
) -> Result<(), validator::ValidationError> {
|
|
|
|
|
if values
|
|
|
|
|
.iter()
|
|
|
|
|
.duplicates_by(|x| {
|
|
|
|
|
format!(
|
2022-06-05 10:42:33 -07:00
|
|
|
"{}-{}-{}",
|
2025-05-22 03:34:36 -05:00
|
|
|
x.version_id.unwrap_or(crate::models::ids::VersionId(0)),
|
|
|
|
|
x.project_id.unwrap_or(crate::models::ids::ProjectId(0)),
|
2022-06-05 10:42:33 -07:00
|
|
|
x.file_name.as_deref().unwrap_or_default()
|
2022-02-26 21:16:11 -07:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.next()
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
return Err(validator::ValidationError::new("duplicate dependency"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2022-08-16 17:42:04 -07:00
|
|
|
|
2022-10-30 23:34:56 -07:00
|
|
|
pub fn validate_url(value: &str) -> Result<(), validator::ValidationError> {
|
2022-08-16 17:42:04 -07:00
|
|
|
let url = url::Url::parse(value)
|
|
|
|
|
.ok()
|
|
|
|
|
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?;
|
|
|
|
|
|
|
|
|
|
if url.scheme() != "https" {
|
|
|
|
|
return Err(validator::ValidationError::new("URL must be https"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2023-03-04 00:20:04 +01:00
|
|
|
|
2023-11-30 23:14:52 -08:00
|
|
|
pub fn validate_url_hashmap_optional_values(
|
|
|
|
|
values: &std::collections::HashMap<String, Option<String>>,
|
|
|
|
|
) -> Result<(), validator::ValidationError> {
|
|
|
|
|
for value in values.values().flatten() {
|
|
|
|
|
validate_url(value)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn validate_url_hashmap_values(
|
|
|
|
|
values: &std::collections::HashMap<String, String>,
|
|
|
|
|
) -> Result<(), validator::ValidationError> {
|
|
|
|
|
for value in values.values() {
|
|
|
|
|
validate_url(value)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-18 16:07:35 -07:00
|
|
|
pub fn validate_no_restricted_scopes(
|
|
|
|
|
value: &Scopes,
|
|
|
|
|
) -> Result<(), validator::ValidationError> {
|
2023-10-30 11:14:38 -05:00
|
|
|
if value.is_restricted() {
|
|
|
|
|
return Err(validator::ValidationError::new(
|
|
|
|
|
"Restricted scopes not allowed",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-04 00:20:04 +01:00
|
|
|
pub fn validate_name(value: &str) -> Result<(), validator::ValidationError> {
|
|
|
|
|
if value.trim().is_empty() {
|
|
|
|
|
return Err(validator::ValidationError::new(
|
|
|
|
|
"Name cannot contain only whitespace.",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_name_with_valid_input() {
|
|
|
|
|
let result = validate_name("My Test mod");
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validate_name_with_invalid_input_returns_error() {
|
|
|
|
|
let result = validate_name(" ");
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|