Skip to main content

actr_config/
actr_raw.rs

1//! Hyper runtime configuration structures - direct TOML mapping for actr.toml
2//!
3//! `RuntimeRawConfig` maps the flat-section layout of `actr.toml`, which contains
4//! deployment, signaling, and observability settings.
5
6use 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/// Direct mapping of actr.toml (runtime configuration, no package info)
17///
18/// Unlike `ManifestRawConfig` which has `[system.signaling]`, `RuntimeRawConfig` uses
19/// flat section names: `[signaling]`, `[deployment]`, etc.
20///
21/// ```toml
22/// edition = 1
23///
24/// [signaling]
25/// url = "ws://localhost:8081/signaling/ws"
26///
27/// [deployment]
28/// realm_id = 33554432
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RuntimeRawConfig {
32    /// Config file format version
33    #[serde(default = "default_edition")]
34    pub edition: u32,
35
36    /// Signaling server connection
37    #[serde(default)]
38    pub signaling: RawSignalingConfig,
39
40    /// AIS (Actor Identity Service) endpoint
41    #[serde(default)]
42    pub ais_endpoint: RawAisEndpointConfig,
43
44    /// Deployment parameters (realm, etc.)
45    #[serde(default)]
46    pub deployment: RawDeploymentConfig,
47
48    /// Service discovery settings
49    #[serde(default)]
50    pub discovery: RawDiscoveryConfig,
51
52    /// WebRTC transport configuration
53    #[serde(default)]
54    pub webrtc: RawWebRtcConfig,
55
56    /// WebSocket direct-connect configuration
57    #[serde(default)]
58    pub websocket: RawWebSocketConfig,
59
60    /// Observability (logging, tracing)
61    #[serde(default)]
62    pub observability: RawObservabilityConfig,
63
64    /// Storage constraints (mailbox etc.)
65    #[serde(default)]
66    pub storage: RawStorageConfig,
67
68    /// Service capabilities (for signaling load balancing)
69    #[serde(default)]
70    pub capabilities: Option<RawCapabilitiesConfig>,
71
72    /// Access control list (raw TOML value, parsed later)
73    #[serde(default)]
74    pub acl: Option<toml::Value>,
75
76    /// Script commands (dev-time only)
77    #[serde(default)]
78    pub scripts: HashMap<String, String>,
79
80    /// Path to the workload package (.actr file)
81    ///
82    /// When specified, the runtime can automatically load the workload package
83    /// without requiring explicit path arguments. Supports both absolute and
84    /// relative paths (relative to the config file directory).
85    ///
86    /// Example:
87    /// ```toml
88    /// [package]
89    /// path = "dist/service.actr"
90    /// ```
91    #[serde(default)]
92    pub package: Option<RawPackagePathConfig>,
93
94    /// Trust anchors for verifying `.actr` package signatures. One entry =
95    /// a single provider; multiple entries = auto-chained (first match wins).
96    ///
97    /// Example:
98    /// ```toml
99    /// [[trust]]
100    /// kind = "static"
101    /// pubkey_file = "public-key.json"
102    ///
103    /// # or
104    /// [[trust]]
105    /// kind = "registry"
106    /// endpoint = "http://ais.example.com/ais"
107    /// ```
108    #[serde(default)]
109    pub trust: Vec<crate::config::TrustAnchor>,
110
111    /// Web server configuration for `actr run --web`
112    ///
113    /// When specified, enables serving the actor as a web application.
114    ///
115    /// Example:
116    /// ```toml
117    /// [web]
118    /// port = 5174
119    /// host = "0.0.0.0"
120    /// static_dir = "public"
121    /// ```
122    #[serde(default)]
123    pub web: Option<RawWebConfig>,
124}
125
126/// Workload package path configuration
127#[derive(Debug, Clone, Serialize, Deserialize, Default)]
128pub struct RawPackagePathConfig {
129    /// Path to the .actr package file (relative to config dir or absolute)
130    #[serde(default)]
131    pub path: Option<std::path::PathBuf>,
132}
133
134/// Service capabilities declaration (reported to signaling for load balancing)
135#[derive(Debug, Clone, Serialize, Deserialize, Default)]
136pub struct RawCapabilitiesConfig {
137    /// Maximum concurrent request handling capacity
138    #[serde(default)]
139    pub max_concurrent_requests: Option<u32>,
140
141    /// Supported version range (e.g., "1.0.0-2.0.0")
142    #[serde(default)]
143    pub version_range: Option<String>,
144
145    /// Deployment region (e.g., "cn-beijing")
146    #[serde(default)]
147    pub region: Option<String>,
148
149    /// Custom tags (key-value pairs)
150    #[serde(default)]
151    pub tags: Option<HashMap<String, String>>,
152}
153
154/// Web server configuration for `actr run --web`
155#[derive(Debug, Clone, Serialize, Deserialize, Default)]
156pub struct RawWebConfig {
157    /// HTTP server port (default: 8080)
158    #[serde(default = "default_web_port")]
159    pub port: u16,
160
161    /// HTTP server bind host (default: "0.0.0.0")
162    #[serde(default = "default_web_host")]
163    pub host: String,
164
165    /// Directory to serve static files from (relative to config dir, default: "public")
166    #[serde(default = "default_web_static_dir")]
167    pub static_dir: String,
168
169    /// URL path to the .actr package (served from static dir, e.g. "/packages/echo-server.actr")
170    pub package_url: Option<String>,
171
172    /// URL path to the shared runtime WASM (e.g. "/packages/actr_sw_host_bg.wasm")
173    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    /// Load actr runtime configuration from file
194    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}