use crate::error::Error;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SettingKey {
DefaultTimeoutSecs,
AgentDefaultsJsonErrors,
RetryDefaultsMaxAttempts,
RetryDefaultsInitialDelayMs,
RetryDefaultsMaxDelayMs,
}
impl SettingKey {
pub const ALL: &'static [Self] = &[
Self::DefaultTimeoutSecs,
Self::AgentDefaultsJsonErrors,
Self::RetryDefaultsMaxAttempts,
Self::RetryDefaultsInitialDelayMs,
Self::RetryDefaultsMaxDelayMs,
];
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::DefaultTimeoutSecs => "default_timeout_secs",
Self::AgentDefaultsJsonErrors => "agent_defaults.json_errors",
Self::RetryDefaultsMaxAttempts => "retry_defaults.max_attempts",
Self::RetryDefaultsInitialDelayMs => "retry_defaults.initial_delay_ms",
Self::RetryDefaultsMaxDelayMs => "retry_defaults.max_delay_ms",
}
}
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::DefaultTimeoutSecs
| Self::RetryDefaultsMaxAttempts
| Self::RetryDefaultsInitialDelayMs
| Self::RetryDefaultsMaxDelayMs => "integer",
Self::AgentDefaultsJsonErrors => "boolean",
}
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::DefaultTimeoutSecs => "Default timeout for API requests in seconds",
Self::AgentDefaultsJsonErrors => "Output errors as JSON by default",
Self::RetryDefaultsMaxAttempts => "Maximum retry attempts (0 = disabled)",
Self::RetryDefaultsInitialDelayMs => "Initial delay between retries in milliseconds",
Self::RetryDefaultsMaxDelayMs => "Maximum delay cap in milliseconds",
}
}
#[must_use]
pub const fn default_value_str(&self) -> &'static str {
match self {
Self::DefaultTimeoutSecs => "30",
Self::AgentDefaultsJsonErrors => "false",
Self::RetryDefaultsMaxAttempts => "0",
Self::RetryDefaultsInitialDelayMs => "500",
Self::RetryDefaultsMaxDelayMs => "30000",
}
}
#[must_use]
pub const fn value_from_config(&self, config: &super::models::GlobalConfig) -> SettingValue {
match self {
Self::DefaultTimeoutSecs => SettingValue::U64(config.default_timeout_secs),
Self::AgentDefaultsJsonErrors => SettingValue::Bool(config.agent_defaults.json_errors),
Self::RetryDefaultsMaxAttempts => {
SettingValue::U64(config.retry_defaults.max_attempts as u64)
}
Self::RetryDefaultsInitialDelayMs => {
SettingValue::U64(config.retry_defaults.initial_delay_ms)
}
Self::RetryDefaultsMaxDelayMs => SettingValue::U64(config.retry_defaults.max_delay_ms),
}
}
}
impl fmt::Display for SettingKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for SettingKey {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"default_timeout_secs" => Ok(Self::DefaultTimeoutSecs),
"agent_defaults.json_errors" => Ok(Self::AgentDefaultsJsonErrors),
"retry_defaults.max_attempts" => Ok(Self::RetryDefaultsMaxAttempts),
"retry_defaults.initial_delay_ms" => Ok(Self::RetryDefaultsInitialDelayMs),
"retry_defaults.max_delay_ms" => Ok(Self::RetryDefaultsMaxDelayMs),
_ => Err(Error::unknown_setting_key(s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SettingValue {
U64(u64),
Bool(bool),
}
const MAX_TIMEOUT_SECS: u64 = 365 * 24 * 60 * 60;
const MAX_RETRY_ATTEMPTS: u64 = 10;
const MAX_INITIAL_DELAY_MS: u64 = 60_000;
const MAX_DELAY_CAP_MS: u64 = 300_000;
impl SettingValue {
pub fn parse_for_key(key: SettingKey, value: &str) -> Result<Self, Error> {
match key {
SettingKey::DefaultTimeoutSecs => {
let parsed = value
.parse::<u64>()
.map_err(|_| Error::invalid_setting_value(key, value))?;
if parsed == 0 {
return Err(Error::setting_value_out_of_range(
key,
value,
"timeout must be greater than 0",
));
}
if parsed > MAX_TIMEOUT_SECS {
return Err(Error::setting_value_out_of_range(
key,
value,
&format!("timeout cannot exceed {MAX_TIMEOUT_SECS} seconds (1 year)"),
));
}
Ok(Self::U64(parsed))
}
SettingKey::AgentDefaultsJsonErrors => {
let parsed = match value.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => true,
"false" | "0" | "no" | "off" => false,
_ => return Err(Error::invalid_setting_value(key, value)),
};
Ok(Self::Bool(parsed))
}
SettingKey::RetryDefaultsMaxAttempts => {
let parsed = value
.parse::<u64>()
.map_err(|_| Error::invalid_setting_value(key, value))?;
if parsed > MAX_RETRY_ATTEMPTS {
return Err(Error::setting_value_out_of_range(
key,
value,
&format!("max_attempts cannot exceed {MAX_RETRY_ATTEMPTS}"),
));
}
Ok(Self::U64(parsed))
}
SettingKey::RetryDefaultsInitialDelayMs => {
let parsed = value
.parse::<u64>()
.map_err(|_| Error::invalid_setting_value(key, value))?;
if parsed == 0 {
return Err(Error::setting_value_out_of_range(
key,
value,
"initial_delay_ms must be greater than 0",
));
}
if parsed > MAX_INITIAL_DELAY_MS {
return Err(Error::setting_value_out_of_range(
key,
value,
&format!(
"initial_delay_ms cannot exceed {MAX_INITIAL_DELAY_MS}ms (1 minute)"
),
));
}
Ok(Self::U64(parsed))
}
SettingKey::RetryDefaultsMaxDelayMs => {
let parsed = value
.parse::<u64>()
.map_err(|_| Error::invalid_setting_value(key, value))?;
if parsed == 0 {
return Err(Error::setting_value_out_of_range(
key,
value,
"max_delay_ms must be greater than 0",
));
}
if parsed > MAX_DELAY_CAP_MS {
return Err(Error::setting_value_out_of_range(
key,
value,
&format!("max_delay_ms cannot exceed {MAX_DELAY_CAP_MS}ms (5 minutes)"),
));
}
Ok(Self::U64(parsed))
}
}
}
#[must_use]
pub const fn as_u64(&self) -> Option<u64> {
match self {
Self::U64(v) => Some(*v),
Self::Bool(_) => None,
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(v) => Some(*v),
Self::U64(_) => None,
}
}
}
impl fmt::Display for SettingValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::U64(v) => write!(f, "{v}"),
Self::Bool(v) => write!(f, "{v}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingInfo {
pub key: String,
pub value: String,
#[serde(rename = "type")]
pub type_name: String,
pub description: String,
pub default: String,
}
impl SettingInfo {
#[must_use]
pub fn new(key: SettingKey, current_value: &SettingValue) -> Self {
Self {
key: key.as_str().to_string(),
value: current_value.to_string(),
type_name: key.type_name().to_string(),
description: key.description().to_string(),
default: key.default_value_str().to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_setting_key_from_str_valid() {
assert_eq!(
"default_timeout_secs".parse::<SettingKey>().unwrap(),
SettingKey::DefaultTimeoutSecs
);
assert_eq!(
"agent_defaults.json_errors".parse::<SettingKey>().unwrap(),
SettingKey::AgentDefaultsJsonErrors
);
}
#[test]
fn test_setting_key_from_str_invalid() {
let result = "unknown_key".parse::<SettingKey>();
assert!(result.is_err());
}
#[test]
fn test_setting_key_as_str() {
assert_eq!(
SettingKey::DefaultTimeoutSecs.as_str(),
"default_timeout_secs"
);
assert_eq!(
SettingKey::AgentDefaultsJsonErrors.as_str(),
"agent_defaults.json_errors"
);
}
#[test]
fn test_setting_value_parse_u64_valid() {
let value = SettingValue::parse_for_key(SettingKey::DefaultTimeoutSecs, "60").unwrap();
assert_eq!(value, SettingValue::U64(60));
}
#[test]
fn test_setting_value_parse_u64_invalid() {
let result = SettingValue::parse_for_key(SettingKey::DefaultTimeoutSecs, "abc");
assert!(result.is_err());
}
#[test]
fn test_setting_value_parse_bool_valid() {
let key = SettingKey::AgentDefaultsJsonErrors;
assert_eq!(
SettingValue::parse_for_key(key, "true").unwrap(),
SettingValue::Bool(true)
);
assert_eq!(
SettingValue::parse_for_key(key, "false").unwrap(),
SettingValue::Bool(false)
);
assert_eq!(
SettingValue::parse_for_key(key, "1").unwrap(),
SettingValue::Bool(true)
);
assert_eq!(
SettingValue::parse_for_key(key, "0").unwrap(),
SettingValue::Bool(false)
);
assert_eq!(
SettingValue::parse_for_key(key, "yes").unwrap(),
SettingValue::Bool(true)
);
assert_eq!(
SettingValue::parse_for_key(key, "no").unwrap(),
SettingValue::Bool(false)
);
}
#[test]
fn test_setting_value_parse_bool_invalid() {
let result = SettingValue::parse_for_key(SettingKey::AgentDefaultsJsonErrors, "maybe");
assert!(result.is_err());
}
#[test]
fn test_setting_value_display() {
assert_eq!(SettingValue::U64(30).to_string(), "30");
assert_eq!(SettingValue::Bool(true).to_string(), "true");
assert_eq!(SettingValue::Bool(false).to_string(), "false");
}
#[test]
fn test_setting_info_new() {
let info = SettingInfo::new(SettingKey::DefaultTimeoutSecs, &SettingValue::U64(60));
assert_eq!(info.key, "default_timeout_secs");
assert_eq!(info.value, "60");
assert_eq!(info.type_name, "integer");
assert_eq!(info.default, "30");
}
#[test]
fn test_setting_value_parse_timeout_zero_rejected() {
let result = SettingValue::parse_for_key(SettingKey::DefaultTimeoutSecs, "0");
assert!(result.is_err());
}
#[test]
fn test_setting_value_parse_timeout_max_boundary() {
let result = SettingValue::parse_for_key(
SettingKey::DefaultTimeoutSecs,
&super::MAX_TIMEOUT_SECS.to_string(),
);
assert!(result.is_ok());
}
#[test]
fn test_setting_value_parse_timeout_over_max_rejected() {
let over_max = super::MAX_TIMEOUT_SECS + 1;
let result =
SettingValue::parse_for_key(SettingKey::DefaultTimeoutSecs, &over_max.to_string());
assert!(result.is_err());
}
#[test]
fn test_retry_settings_from_str() {
assert_eq!(
"retry_defaults.max_attempts".parse::<SettingKey>().unwrap(),
SettingKey::RetryDefaultsMaxAttempts
);
assert_eq!(
"retry_defaults.initial_delay_ms"
.parse::<SettingKey>()
.unwrap(),
SettingKey::RetryDefaultsInitialDelayMs
);
assert_eq!(
"retry_defaults.max_delay_ms".parse::<SettingKey>().unwrap(),
SettingKey::RetryDefaultsMaxDelayMs
);
}
#[test]
fn test_retry_max_attempts_valid_range() {
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxAttempts, "0");
assert_eq!(result.unwrap(), SettingValue::U64(0));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxAttempts, "3");
assert_eq!(result.unwrap(), SettingValue::U64(3));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxAttempts, "10");
assert_eq!(result.unwrap(), SettingValue::U64(10));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxAttempts, "11");
assert!(result.is_err());
}
#[test]
fn test_retry_initial_delay_ms_valid_range() {
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsInitialDelayMs, "0");
assert!(result.is_err());
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsInitialDelayMs, "100");
assert_eq!(result.unwrap(), SettingValue::U64(100));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsInitialDelayMs, "60000");
assert_eq!(result.unwrap(), SettingValue::U64(60000));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsInitialDelayMs, "60001");
assert!(result.is_err());
}
#[test]
fn test_retry_max_delay_ms_valid_range() {
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxDelayMs, "0");
assert!(result.is_err());
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxDelayMs, "1000");
assert_eq!(result.unwrap(), SettingValue::U64(1000));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxDelayMs, "300000");
assert_eq!(result.unwrap(), SettingValue::U64(300_000));
let result = SettingValue::parse_for_key(SettingKey::RetryDefaultsMaxDelayMs, "300001");
assert!(result.is_err());
}
#[test]
fn test_retry_settings_descriptions() {
assert_eq!(
SettingKey::RetryDefaultsMaxAttempts.description(),
"Maximum retry attempts (0 = disabled)"
);
assert_eq!(
SettingKey::RetryDefaultsInitialDelayMs.description(),
"Initial delay between retries in milliseconds"
);
assert_eq!(
SettingKey::RetryDefaultsMaxDelayMs.description(),
"Maximum delay cap in milliseconds"
);
}
#[test]
fn test_retry_settings_defaults() {
assert_eq!(
SettingKey::RetryDefaultsMaxAttempts.default_value_str(),
"0"
);
assert_eq!(
SettingKey::RetryDefaultsInitialDelayMs.default_value_str(),
"500"
);
assert_eq!(
SettingKey::RetryDefaultsMaxDelayMs.default_value_str(),
"30000"
);
}
}