1use crate::error::{ConfigError, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[serde(deny_unknown_fields)]
12pub struct CliConfig {
13 pub version: Option<u32>,
15
16 #[serde(default)]
18 pub mfr: MfrConfig,
19
20 #[serde(default)]
22 pub codegen: CodegenConfig,
23
24 #[serde(default)]
26 pub install: InstallConfig,
27
28 #[serde(default)]
30 pub cache: CacheConfig,
31
32 #[serde(default)]
34 pub ui: UiConfig,
35
36 #[serde(default)]
38 pub network: NetworkConfig,
39
40 #[serde(default)]
42 pub storage: StorageConfig,
43}
44
45impl CliConfig {
46 pub fn validate(&self) -> Result<()> {
48 if let Some(v) = self.version {
49 if v != 1 {
50 return Err(ConfigError::ValidationError(format!(
51 "Unsupported config version: {}. Supported version is 1",
52 v
53 )));
54 }
55 }
56
57 if let Some(ref manufacturer) = self.mfr.manufacturer {
58 if manufacturer.trim().is_empty() {
59 return Err(ConfigError::ValidationError(
60 "mfr.manufacturer cannot be empty".to_string(),
61 ));
62 }
63 }
64
65 if let Some(ref keychain) = self.mfr.keychain {
66 if keychain.trim().is_empty() {
67 return Err(ConfigError::ValidationError(
68 "mfr.keychain cannot be empty string (omit the field instead)".to_string(),
69 ));
70 }
71 }
72
73 if let Some(ref language) = self.codegen.language {
74 let valid_languages = ["rust", "typescript", "swift", "kotlin", "python", "web"];
75 if !valid_languages.contains(&language.as_str()) {
76 return Err(ConfigError::ValidationError(format!(
77 "codegen.language '{}' is invalid. Valid values: {}",
78 language,
79 valid_languages.join(", ")
80 )));
81 }
82 }
83
84 if let Some(ref format) = self.ui.format {
85 let valid_formats = ["toml", "json", "yaml"];
86 if !valid_formats.contains(&format.as_str()) {
87 return Err(ConfigError::ValidationError(format!(
88 "ui.format '{}' is invalid. Valid values: {}",
89 format,
90 valid_formats.join(", ")
91 )));
92 }
93 }
94
95 if let Some(ref color) = self.ui.color {
96 let valid_colors = ["auto", "always", "never"];
97 if !valid_colors.contains(&color.as_str()) {
98 return Err(ConfigError::ValidationError(format!(
99 "ui.color '{}' is invalid. Valid values: {}",
100 color,
101 valid_colors.join(", ")
102 )));
103 }
104 }
105
106 if let Some(ref url) = self.network.signaling_url {
107 if url.trim().is_empty() {
108 return Err(ConfigError::ValidationError(
109 "network.signaling_url cannot be empty".to_string(),
110 ));
111 }
112 if !url.starts_with("ws://") && !url.starts_with("wss://") {
113 return Err(ConfigError::ValidationError(format!(
114 "network.signaling_url '{}' must start with ws:// or wss://",
115 url
116 )));
117 }
118 }
119
120 if let Some(ref url) = self.network.ais_endpoint {
121 if url.trim().is_empty() {
122 return Err(ConfigError::ValidationError(
123 "network.ais_endpoint cannot be empty".to_string(),
124 ));
125 }
126 if !url.starts_with("http://") && !url.starts_with("https://") {
127 return Err(ConfigError::ValidationError(format!(
128 "network.ais_endpoint '{}' must start with http:// or https://",
129 url
130 )));
131 }
132 }
133
134 if let Some(realm_id) = self.network.realm_id {
135 if realm_id == 0 {
136 return Err(ConfigError::ValidationError(
137 "network.realm_id must be a positive integer".to_string(),
138 ));
139 }
140 }
141
142 if let Some(ref secret) = self.network.realm_secret {
143 if secret.is_empty() {
144 return Err(ConfigError::ValidationError(
145 "network.realm_secret cannot be empty string (omit the field instead)"
146 .to_string(),
147 ));
148 }
149 }
150
151 Ok(())
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157#[serde(deny_unknown_fields)]
158pub struct MfrConfig {
159 pub manufacturer: Option<String>,
161
162 pub keychain: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, Default)]
168#[serde(deny_unknown_fields)]
169pub struct CodegenConfig {
170 pub language: Option<String>,
172
173 pub output: Option<String>,
175
176 pub clean_before_generate: Option<bool>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
182#[serde(deny_unknown_fields)]
183pub struct InstallConfig {}
184
185#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187#[serde(deny_unknown_fields)]
188pub struct CacheConfig {
189 pub dir: Option<String>,
191
192 pub auto_lock: Option<bool>,
194
195 pub prefer_cache: Option<bool>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
201#[serde(deny_unknown_fields)]
202pub struct UiConfig {
203 pub format: Option<String>,
205
206 pub verbose: Option<bool>,
208
209 pub color: Option<String>,
211
212 pub non_interactive: Option<bool>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, Default)]
218#[serde(deny_unknown_fields)]
219pub struct NetworkConfig {
220 pub signaling_url: Option<String>,
222
223 pub ais_endpoint: Option<String>,
225
226 pub realm_id: Option<u32>,
228
229 pub realm_secret: Option<String>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, Default)]
235#[serde(deny_unknown_fields)]
236pub struct StorageConfig {
237 pub hyper_data_dir: Option<String>,
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn validate_accepts_storage_hyper_dir() {
247 let config = CliConfig {
248 storage: StorageConfig {
249 hyper_data_dir: Some("~/.actr/hyper".to_string()),
250 },
251 ..Default::default()
252 };
253
254 assert!(config.validate().is_ok());
255 }
256
257 #[test]
258 fn validate_rejects_invalid_version() {
259 let config = CliConfig {
260 version: Some(2),
261 ..Default::default()
262 };
263
264 assert!(config.validate().is_err());
265 }
266}