1use crate::error::Result;
7use crate::raw::{
8 RawAisEndpointConfig, RawDeploymentConfig, RawDiscoveryConfig, RawObservabilityConfig,
9 RawSignalingConfig, RawStorageConfig, RawWebRtcConfig, RawWebSocketConfig,
10};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14use std::str::FromStr;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RuntimeRawConfig {
32 #[serde(default = "default_edition")]
34 pub edition: u32,
35
36 #[serde(default)]
38 pub signaling: RawSignalingConfig,
39
40 #[serde(default)]
42 pub ais_endpoint: RawAisEndpointConfig,
43
44 #[serde(default)]
46 pub deployment: RawDeploymentConfig,
47
48 #[serde(default)]
50 pub discovery: RawDiscoveryConfig,
51
52 #[serde(default)]
54 pub webrtc: RawWebRtcConfig,
55
56 #[serde(default)]
58 pub websocket: RawWebSocketConfig,
59
60 #[serde(default)]
62 pub observability: RawObservabilityConfig,
63
64 #[serde(default)]
66 pub storage: RawStorageConfig,
67
68 #[serde(default)]
70 pub capabilities: Option<RawCapabilitiesConfig>,
71
72 #[serde(default)]
74 pub acl: Option<toml::Value>,
75
76 #[serde(default)]
78 pub scripts: HashMap<String, String>,
79
80 #[serde(default)]
92 pub package: Option<RawPackagePathConfig>,
93
94 #[serde(default)]
109 pub trust: Vec<crate::config::TrustAnchor>,
110
111 #[serde(default)]
123 pub web: Option<RawWebConfig>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, Default)]
128pub struct RawPackagePathConfig {
129 #[serde(default)]
131 pub path: Option<std::path::PathBuf>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, Default)]
136pub struct RawCapabilitiesConfig {
137 #[serde(default)]
139 pub max_concurrent_requests: Option<u32>,
140
141 #[serde(default)]
143 pub version_range: Option<String>,
144
145 #[serde(default)]
147 pub region: Option<String>,
148
149 #[serde(default)]
151 pub tags: Option<HashMap<String, String>>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, Default)]
156pub struct RawWebConfig {
157 #[serde(default = "default_web_port")]
159 pub port: u16,
160
161 #[serde(default = "default_web_host")]
163 pub host: String,
164
165 #[serde(default = "default_web_static_dir")]
167 pub static_dir: String,
168
169 pub package_url: Option<String>,
171
172 pub runtime_wasm_url: Option<String>,
174}
175
176fn default_web_port() -> u16 {
177 8080
178}
179
180fn default_web_host() -> String {
181 "0.0.0.0".to_string()
182}
183
184fn default_web_static_dir() -> String {
185 "public".to_string()
186}
187
188fn default_edition() -> u32 {
189 1
190}
191
192impl RuntimeRawConfig {
193 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
195 let content = std::fs::read_to_string(path)?;
196 content.parse()
197 }
198}
199
200impl FromStr for RuntimeRawConfig {
201 type Err = crate::error::ConfigError;
202
203 fn from_str(s: &str) -> Result<Self> {
204 toml::from_str(s).map_err(Into::into)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_parse_minimal_actr_config() {
214 let toml_content = r#"
215edition = 1
216
217[signaling]
218url = "ws://localhost:8081/signaling/ws"
219
220[deployment]
221realm_id = 1001
222"#;
223 let config = RuntimeRawConfig::from_str(toml_content).unwrap();
224 assert_eq!(config.edition, 1);
225 assert_eq!(
226 config.signaling.url.as_deref(),
227 Some("ws://localhost:8081/signaling/ws")
228 );
229 assert_eq!(config.deployment.realm_id, Some(1001));
230 }
231
232 #[test]
233 fn test_parse_full_actr_config() {
234 let toml_content = r#"
235edition = 1
236
237[signaling]
238url = "ws://localhost:8081/signaling/ws"
239
240[ais_endpoint]
241url = "http://localhost:8081/ais"
242
243[deployment]
244realm_id = 33554432
245realm_secret = "rs_test123"
246
247[discovery]
248visible = true
249
250[webrtc]
251force_relay = false
252stun_urls = ["stun:localhost:3478"]
253turn_urls = ["turn:localhost:3478"]
254
255[websocket]
256listen_port = 9001
257advertised_host = "127.0.0.1"
258
259[observability]
260filter_level = "info"
261tracing_enabled = false
262
263[capabilities]
264max_concurrent_requests = 100
265version_range = "1.0.0-2.0.0"
266region = "cn-beijing"
267
268[capabilities.tags]
269env = "prod"
270tier = "premium"
271
272[acl]
273
274[[acl.rules]]
275permission = "allow"
276type = "acme:EchoService:1.0.0"
277
278[scripts]
279dev = "cargo run"
280test = "cargo test"
281"#;
282 let config = RuntimeRawConfig::from_str(toml_content).unwrap();
283 assert_eq!(config.edition, 1);
284 assert_eq!(
285 config.ais_endpoint.url.as_deref(),
286 Some("http://localhost:8081/ais")
287 );
288 assert_eq!(
289 config.deployment.realm_secret.as_deref(),
290 Some("rs_test123")
291 );
292 assert_eq!(config.discovery.visible, Some(true));
293 assert!(!config.webrtc.force_relay);
294 assert_eq!(config.webrtc.stun_urls.len(), 1);
295 assert_eq!(config.websocket.listen_port, Some(9001));
296 assert_eq!(config.observability.filter_level.as_deref(), Some("info"));
297
298 let caps = config.capabilities.unwrap();
299 assert_eq!(caps.max_concurrent_requests, Some(100));
300 assert_eq!(caps.region.as_deref(), Some("cn-beijing"));
301 assert_eq!(
302 caps.tags
303 .as_ref()
304 .and_then(|t| t.get("env"))
305 .map(|s| s.as_str()),
306 Some("prod")
307 );
308
309 assert!(config.acl.is_some());
310 assert_eq!(
311 config.scripts.get("dev").map(|s| s.as_str()),
312 Some("cargo run")
313 );
314 }
315
316 #[test]
317 fn test_parse_empty_actr_config() {
318 let toml_content = "edition = 1\n";
319 let config = RuntimeRawConfig::from_str(toml_content).unwrap();
320 assert_eq!(config.edition, 1);
321 assert!(config.signaling.url.is_none());
322 assert!(config.capabilities.is_none());
323 }
324
325 #[test]
326 fn test_parse_actr_config_with_package_path() {
327 let toml_content = r#"
328edition = 1
329
330[signaling]
331url = "ws://localhost:8081/signaling/ws"
332
333[deployment]
334realm_id = 1001
335
336[package]
337path = "dist/service.actr"
338"#;
339 let config = RuntimeRawConfig::from_str(toml_content).unwrap();
340 assert_eq!(config.edition, 1);
341 assert!(config.package.is_some());
342 let package = config.package.unwrap();
343 assert_eq!(
344 package.path.as_ref().map(|p| p.to_str().unwrap()),
345 Some("dist/service.actr")
346 );
347 }
348
349 #[test]
350 fn test_reject_runtime_hyper_data_dir() {
351 let toml_content = r#"
352edition = 1
353
354[signaling]
355url = "ws://localhost:8081/signaling/ws"
356
357[deployment]
358realm_id = 1001
359
360[storage]
361hyper_data_dir = ".hyper"
362"#;
363
364 let error = RuntimeRawConfig::from_str(toml_content).unwrap_err();
365 assert!(
366 error.to_string().contains("unknown field `hyper_data_dir`"),
367 "unexpected error: {error}"
368 );
369 }
370}