use std::collections::HashSet;
use std::env;
use std::sync::LazyLock;
use validator::ValidationError;
type Result<T> = std::result::Result<T, ValidationError>;
mod common;
mod user;
fn env_key(code: &str) -> String {
code.replace('-', "_").to_lowercase()
}
fn collect_disabled(lookup: impl Fn(&str) -> Option<String>) -> HashSet<&'static str> {
ALL_CODES
.iter()
.copied()
.filter(|code| lookup(&env_key(code)).as_deref() == Some("*"))
.collect()
}
static DISABLED_CODES: LazyLock<HashSet<&'static str>> =
LazyLock::new(|| collect_disabled(|key| env::var(key).ok()));
fn is_disabled(code: &str) -> bool {
DISABLED_CODES.contains(code)
}
fn new_error(code: &'static str, message: String) -> ValidationError {
ValidationError::new(code).with_message(message.into())
}
pub(super) fn is_identifier_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.')
}
pub(super) fn has_control_or_space(value: &str) -> bool {
value.chars().any(|c| c.is_ascii_control() || c == ' ')
}
fn validate_ascii_name(
name: &str,
code: &'static str,
max_len: usize,
field_name: &str,
) -> Result<()> {
if name.is_empty() {
return Err(new_error(code, format!("{field_name} cannot be empty")));
}
if !name.is_ascii() {
return Err(new_error(code, format!("{field_name} must be ASCII")));
}
if name.len() > max_len {
return Err(new_error(
code,
format!("{field_name} must be less than {max_len} characters"),
));
}
if !name.chars().all(is_identifier_char) {
return Err(new_error(
code,
format!("{field_name} may only contain letters, digits, '_', '-' and '.'"),
));
}
Ok(())
}
macro_rules! validate_codes {
($($(#[$attr:meta])* $name:ident = $value:literal;)+) => {
$($(#[$attr])* pub const $name: &str = $value;)+
const ALL_CODES: &[&str] = &[$($name),+];
};
}
validate_codes! {
CODE_USER_ACCOUNT = "x-user-account";
CODE_USER_ACCOUNT_STRICT = "x-user-account-strict";
CODE_USER_PASSWORD = "x-user-password";
CODE_USER_EMAIL = "x-user-email";
CODE_USER_ROLES = "x-user-roles";
CODE_USER_GROUPS = "x-user-groups";
CODE_LISTEN_ADDR = "x-listen-addr";
CODE_UUID = "x-uuid";
CODE_SHA256 = "x-sha256";
CODE_FILE_NAME = "x-file-name";
CODE_FILE_GROUP = "x-file-group";
CODE_SCHEMA_NAME = "x-schema-name";
CODE_IMAGE_FORMAT = "x-image-format";
CODE_IMAGE_QUALITY = "x-image-quality";
}
pub use common::*;
pub use user::*;
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
#[test]
fn env_key_maps_code_to_variable_name() {
assert_eq!(env_key("x-user-account"), "x_user_account");
assert_eq!(env_key("x-uuid"), "x_uuid");
assert_eq!(env_key("X-Image-Quality"), "x_image_quality");
}
#[test]
fn all_codes_registry_is_consistent() {
let unique: HashSet<&&str> = ALL_CODES.iter().collect();
assert_eq!(unique.len(), ALL_CODES.len(), "ALL_CODES 不得有重复项");
for code in ALL_CODES {
assert!(code.starts_with("x-"), "{code} 应以 x- 开头");
assert_eq!(*code, code.to_lowercase(), "{code} 应全小写");
}
}
#[test]
fn only_star_disables_a_validator() {
let env: HashMap<String, String> = [
("x_user_account", "*"),
("x_uuid", "1"), ("x_sha256", ""), ("x_unrelated", "*"), ]
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
let disabled = collect_disabled(|key| env.get(key).cloned());
assert_eq!(disabled, HashSet::from([CODE_USER_ACCOUNT]));
}
#[test]
fn empty_environment_disables_nothing() {
assert!(collect_disabled(|_| None).is_empty());
}
#[test]
fn every_registered_code_is_switchable() {
for code in ALL_CODES {
let key = env_key(code);
let disabled = collect_disabled(|k| (k == key).then(|| "*".to_string()));
assert_eq!(
disabled,
HashSet::from([*code]),
"{code} 的开关未生效(可能未登记进 ALL_CODES)"
);
}
}
}