use crate::config_error::ConfigError;
use monochromatic_jsonc_edit::{JsoncEntry, JsoncKind, JsoncValue, units_to_string};
pub(crate) const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
pub(crate) fn member_path(parent: &str, key: &str) -> String {
if parent.is_empty() {
return String::from(key);
}
return format!("{parent}.{key}");
}
pub(crate) fn kind_name(value: &JsoncValue) -> &'static str {
match &value.kind {
JsoncKind::Null => return "null",
JsoncKind::Boolean { .. } => return "a boolean",
JsoncKind::Number { .. } => return "a number",
JsoncKind::Text { .. } => return "a string",
JsoncKind::Array { .. } => return "an array",
JsoncKind::Record { .. } => return "an object",
}
}
pub(crate) fn wrong_kind(value: &JsoncValue, path: &str, expected: &str) -> ConfigError {
let subject: String = if path.is_empty() {
String::from("The configuration document")
} else {
format!("Configuration key {path}")
};
if let JsoncKind::Null = value.kind {
return ConfigError::new(
format!(
"{subject} must not be null; remove it to use the default or give it {expected}."
)
.as_str(),
);
}
return ConfigError::new(
format!("{subject} must be {expected}, found {}.", kind_name(value)).as_str(),
);
}
pub(crate) fn member_keys(entries: &[JsoncEntry], path: &str) -> Result<Vec<String>, ConfigError> {
let mut keys: Vec<String> = Vec::<String>::with_capacity(entries.len());
for entry in entries {
let key: String = match units_to_string(&entry.key.units) {
Ok(decoded) => decoded,
Err(_) => {
return Err(ConfigError::new(
format!(
"A key under {} is not valid Unicode text (unpaired UTF-16 surrogate): {}.",
container_name(path),
String::from_utf16_lossy(&entry.key.units)
)
.as_str(),
));
}
};
if keys.contains(&key) {
return Err(ConfigError::new(
format!(
"Configuration key {} is defined more than once; keep exactly one definition.",
member_path(path, key.as_str())
)
.as_str(),
));
}
keys.push(key);
}
return Ok(keys);
}
fn container_name(path: &str) -> String {
if path.is_empty() {
return String::from("the configuration document");
}
return String::from(path);
}
pub(crate) fn boolean(value: &JsoncValue, path: &str) -> Result<bool, ConfigError> {
if let JsoncKind::Boolean { value: flag } = value.kind {
return Ok(flag);
}
return Err(wrong_kind(value, path, "true or false"));
}
pub(crate) fn text(value: &JsoncValue, path: &str) -> Result<String, ConfigError> {
let Some(units) = value.text_units() else {
return Err(wrong_kind(value, path, "a string"));
};
match units_to_string(units) {
Ok(decoded) => return Ok(decoded),
Err(_) => {
return Err(ConfigError::new(
format!(
"Configuration key {path} is not valid Unicode text (unpaired UTF-16 surrogate): {}.",
String::from_utf16_lossy(units)
)
.as_str(),
));
}
}
}
pub(crate) fn strings(value: &JsoncValue, path: &str) -> Result<Vec<String>, ConfigError> {
let Some(elements) = value.elements() else {
return Err(wrong_kind(value, path, "an array of strings"));
};
let mut result: Vec<String> = Vec::<String>::with_capacity(elements.len());
for (index, element) in elements.iter().enumerate() {
let element_path: String = format!("{path}[{index}]");
result.push(text(element, element_path.as_str())?);
}
return Ok(result);
}
pub(crate) fn safe_integer(
value: &JsoncValue,
path: &str,
minimum: u64,
) -> Result<u64, ConfigError> {
let expected: String = format!("a whole number from {minimum} to {MAX_SAFE_INTEGER}");
let JsoncKind::Number { identity, .. } = &value.kind else {
return Err(wrong_kind(value, path, expected.as_str()));
};
let failure: ConfigError = ConfigError::new(
format!(
"Configuration key {path} must be {expected}, found {}.",
value.number_token().unwrap_or("a number")
)
.as_str(),
);
if identity.is_negative() {
return Err(failure);
}
let exact: u64 = if identity.is_zero() {
0
} else {
let (Ok(coefficient), Ok(exponent)) = (
identity.digits().parse::<u64>(),
identity.exponent().parse::<u32>(),
) else {
return Err(failure);
};
let Some(scale) = 10_u64.checked_pow(exponent) else {
return Err(failure);
};
let Some(product) = coefficient.checked_mul(scale) else {
return Err(failure);
};
product
};
if exact < minimum || exact > MAX_SAFE_INTEGER {
return Err(failure);
}
return Ok(exact);
}
#[cfg(test)]
#[path = "config_values_tests.rs"]
mod tests;