use serde::Serialize;
#[derive(Clone, Debug, Serialize)]
pub(crate) struct ResourceConfig {
pub(crate) memory: String,
pub(crate) cpu: String,
pub(crate) idle_timeout_minutes: u16,
}
pub(crate) fn parse_resource_config(
table: &toml::Table,
) -> std::result::Result<ResourceConfig, String> {
Ok(ResourceConfig {
memory: get_section_string(table, "memory")?.unwrap_or_else(|| "512mb".to_owned()),
cpu: get_section_string(table, "cpu")?.unwrap_or_else(|| "0.25".to_owned()),
idle_timeout_minutes: get_section_u16(table, "idle_timeout_minutes")?.unwrap_or(15),
})
}
pub(crate) fn validate_resource_config(
resources: &ResourceConfig,
) -> std::result::Result<(), String> {
let memory_mib = memory_to_mib(&resources.memory)?;
if !(128..=2048).contains(&memory_mib) {
return Err(
"[resources].memory must be between 128mb and 2gb; use the smallest working value"
.to_owned(),
);
}
let cpu_millis = cpu_to_millis(&resources.cpu)?;
if !(50..=2000).contains(&cpu_millis) {
return Err(
"[resources].cpu must be between 0.05 and 2; use the smallest working value".to_owned(),
);
}
if !(1..=60).contains(&resources.idle_timeout_minutes) {
return Err("[resources].idle_timeout_minutes must be between 1 and 60".to_owned());
}
Ok(())
}
fn get_section_string(
table: &toml::map::Map<String, toml::Value>,
key: &str,
) -> std::result::Result<Option<String>, String> {
table.get(key).map_or(Ok(None), |value| {
value
.as_str()
.map(|value| Some(value.to_owned()))
.ok_or_else(|| format!("{key} must be a string"))
})
}
fn get_section_u16(
table: &toml::map::Map<String, toml::Value>,
key: &str,
) -> std::result::Result<Option<u16>, String> {
table.get(key).map_or(Ok(None), |value| {
let integer = value
.as_integer()
.ok_or_else(|| format!("{key} must be a number"))?;
u16::try_from(integer)
.ok()
.map(Some)
.ok_or_else(|| format!("{key} must be between 0 and 65535"))
})
}
fn memory_to_mib(value: &str) -> std::result::Result<u32, String> {
let clean = value.trim().to_ascii_lowercase();
let amount = clean
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>();
let unit = clean[amount.len()..].trim();
let amount = amount
.parse::<u32>()
.map_err(|_error| "[resources].memory must look like 256mb, 512mb, or 1gb".to_owned())?;
match unit {
"mb" | "mib" => Ok(amount),
"gb" | "gib" => Ok(amount * 1024),
_ => Err("[resources].memory must look like 256mb, 512mb, or 1gb".to_owned()),
}
}
fn cpu_to_millis(value: &str) -> std::result::Result<u32, String> {
let clean = value.trim();
if clean.is_empty()
|| clean
.chars()
.any(|character| !character.is_ascii_digit() && character != '.')
|| clean.matches('.').count() > 1
{
return Err("[resources].cpu must look like 0.25, 0.5, 1, or 2".to_owned());
}
let mut parts = clean.split('.');
let whole = parts
.next()
.unwrap_or_default()
.parse::<u32>()
.map_err(|_error| "[resources].cpu must look like 0.25, 0.5, 1, or 2".to_owned())?;
let fraction = parts.next().unwrap_or_default();
if parts.next().is_some() || fraction.len() > 3 {
return Err("[resources].cpu must look like 0.25, 0.5, 1, or 2".to_owned());
}
let mut fractional_millis = 0u32;
for (index, digit) in fraction.bytes().enumerate() {
fractional_millis += u32::from(digit - b'0') * [100, 10, 1][index];
}
Ok(whole * 1000 + fractional_millis)
}