use crate::error::{ConfigError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct CliConfig {
pub version: Option<u32>,
#[serde(default)]
pub mfr: MfrConfig,
#[serde(default)]
pub codegen: CodegenConfig,
#[serde(default)]
pub install: InstallConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub ui: UiConfig,
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub storage: StorageConfig,
}
impl CliConfig {
pub fn validate(&self) -> Result<()> {
if let Some(v) = self.version {
if v != 1 {
return Err(ConfigError::ValidationError(format!(
"Unsupported config version: {}. Supported version is 1",
v
)));
}
}
if let Some(ref manufacturer) = self.mfr.manufacturer {
if manufacturer.trim().is_empty() {
return Err(ConfigError::ValidationError(
"mfr.manufacturer cannot be empty".to_string(),
));
}
}
if let Some(ref keychain) = self.mfr.keychain {
if keychain.trim().is_empty() {
return Err(ConfigError::ValidationError(
"mfr.keychain cannot be empty string (omit the field instead)".to_string(),
));
}
}
if let Some(ref language) = self.codegen.language {
let valid_languages = ["rust", "typescript", "swift", "kotlin", "python", "web"];
if !valid_languages.contains(&language.as_str()) {
return Err(ConfigError::ValidationError(format!(
"codegen.language '{}' is invalid. Valid values: {}",
language,
valid_languages.join(", ")
)));
}
}
if let Some(ref format) = self.ui.format {
let valid_formats = ["toml", "json", "yaml"];
if !valid_formats.contains(&format.as_str()) {
return Err(ConfigError::ValidationError(format!(
"ui.format '{}' is invalid. Valid values: {}",
format,
valid_formats.join(", ")
)));
}
}
if let Some(ref color) = self.ui.color {
let valid_colors = ["auto", "always", "never"];
if !valid_colors.contains(&color.as_str()) {
return Err(ConfigError::ValidationError(format!(
"ui.color '{}' is invalid. Valid values: {}",
color,
valid_colors.join(", ")
)));
}
}
if let Some(ref url) = self.network.signaling_url {
if url.trim().is_empty() {
return Err(ConfigError::ValidationError(
"network.signaling_url cannot be empty".to_string(),
));
}
if !url.starts_with("ws://") && !url.starts_with("wss://") {
return Err(ConfigError::ValidationError(format!(
"network.signaling_url '{}' must start with ws:// or wss://",
url
)));
}
}
if let Some(ref url) = self.network.ais_endpoint {
if url.trim().is_empty() {
return Err(ConfigError::ValidationError(
"network.ais_endpoint cannot be empty".to_string(),
));
}
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ConfigError::ValidationError(format!(
"network.ais_endpoint '{}' must start with http:// or https://",
url
)));
}
}
if let Some(realm_id) = self.network.realm_id {
if realm_id == 0 {
return Err(ConfigError::ValidationError(
"network.realm_id must be a positive integer".to_string(),
));
}
}
if let Some(ref secret) = self.network.realm_secret {
if secret.is_empty() {
return Err(ConfigError::ValidationError(
"network.realm_secret cannot be empty string (omit the field instead)"
.to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct MfrConfig {
pub manufacturer: Option<String>,
pub keychain: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct CodegenConfig {
pub language: Option<String>,
pub output: Option<String>,
pub clean_before_generate: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct InstallConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct CacheConfig {
pub dir: Option<String>,
pub auto_lock: Option<bool>,
pub prefer_cache: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct UiConfig {
pub format: Option<String>,
pub verbose: Option<bool>,
pub color: Option<String>,
pub non_interactive: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct NetworkConfig {
pub signaling_url: Option<String>,
pub ais_endpoint: Option<String>,
pub realm_id: Option<u32>,
pub realm_secret: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct StorageConfig {
pub hyper_data_dir: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_accepts_storage_hyper_dir() {
let config = CliConfig {
storage: StorageConfig {
hyper_data_dir: Some("~/.actr/hyper".to_string()),
},
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn validate_rejects_invalid_version() {
let config = CliConfig {
version: Some(2),
..Default::default()
};
assert!(config.validate().is_err());
}
}