Skip to main content

actr_config/
raw.rs

1//! Raw configuration structures - direct TOML mapping
2
3use crate::error::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9/// Direct mapping of manifest.toml (no processing applied)
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ManifestRawConfig {
12    /// Config file format version (determines which Parser to use)
13    #[serde(default = "default_edition")]
14    pub edition: u32,
15
16    /// Inherited parent config file path
17    #[serde(default)]
18    pub inherit: Option<PathBuf>,
19
20    /// Directory containing the lock file
21    #[serde(default)]
22    pub config_dir: Option<PathBuf>,
23
24    /// Package info
25    pub package: RawPackageConfig,
26
27    /// Exported proto file list
28    #[serde(default)]
29    pub exports: Vec<PathBuf>,
30
31    /// Service dependencies
32    #[serde(default)]
33    pub dependencies: HashMap<String, RawDependency>,
34
35    /// Access control list (raw TOML value, parsed later)
36    #[serde(default)]
37    pub acl: Option<toml::Value>,
38
39    /// Script commands
40    #[serde(default)]
41    pub scripts: HashMap<String, String>,
42
43    /// Final packaged binary configuration
44    #[serde(default)]
45    pub binary: Option<RawBinaryConfig>,
46
47    /// Source build configuration
48    #[serde(default)]
49    pub build: Option<RawBuildConfig>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct RawPackageConfig {
54    /// Package name (also used as the actor type name)
55    pub name: String,
56
57    /// Manufacturer identifier (e.g., "acme")
58    pub manufacturer: String,
59
60    /// Semantic version (e.g., "1.0.0"). Defaults to empty string if not specified.
61    #[serde(default)]
62    pub version: String,
63
64    #[serde(default)]
65    pub description: Option<String>,
66
67    #[serde(default)]
68    pub authors: Option<Vec<String>>,
69
70    #[serde(default)]
71    pub license: Option<String>,
72
73    /// Service tags (e.g., ["latest", "stable", "v1.0"])
74    #[serde(default)]
75    pub tags: Vec<String>,
76
77    /// Signature algorithm (default: "ed25519")
78    #[serde(default)]
79    pub signature_algorithm: Option<String>,
80
81    /// Exported proto file paths (new location, preferred over top-level `exports`)
82    #[serde(default)]
83    pub exports: Vec<PathBuf>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RawBinaryConfig {
88    pub path: PathBuf,
89
90    #[serde(default)]
91    pub target: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, Default)]
95pub struct RawBuildConfig {
96    #[serde(default)]
97    pub tool: Option<String>,
98
99    #[serde(default)]
100    pub manifest_path: Option<PathBuf>,
101
102    #[serde(default)]
103    pub artifact: Option<String>,
104
105    #[serde(default)]
106    pub target: Option<String>,
107
108    #[serde(default)]
109    pub profile: Option<String>,
110
111    #[serde(default)]
112    pub features: Vec<String>,
113
114    #[serde(default)]
115    pub no_default_features: bool,
116
117    #[serde(default)]
118    pub post_build: Vec<String>,
119}
120
121impl RawPackageConfig {
122    pub fn into_package_info(self) -> Result<crate::config::PackageInfo> {
123        Ok(crate::config::PackageInfo {
124            name: self.name.clone(),
125            actr_type: actr_protocol::ActrType {
126                manufacturer: self.manufacturer.clone(),
127                name: self.name,
128                version: self.version,
129            },
130            description: self.description,
131            authors: self.authors.unwrap_or_default(),
132            license: self.license,
133        })
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum RawDependency {
140    /// Dependency with specified ActrType (matched first since it has the required `actr_type` field)
141    ///
142    /// Example:
143    /// ```toml
144    /// [dependencies]
145    /// echo = { actr_type = "acme:echo-service:1.0.0", service = "EchoService:abc1f3d" }
146    /// ```
147    Specified {
148        /// Full ActrType string: "manufacturer:name:version"
149        #[serde(rename = "actr_type")]
150        actr_type: String,
151
152        /// Optional strict service reference: "ServiceName:fingerprint".
153        /// When present, enables exact proto fingerprint matching.
154        #[serde(default)]
155        service: Option<String>,
156
157        /// Optional cross-realm override. Defaults to self realm.
158        #[serde(default)]
159        realm: Option<u32>,
160    },
161
162    /// Empty dependency declaration: {} (populated by actr deps install)
163    Empty {},
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, Default)]
167pub struct RawSystemConfig {
168    #[serde(default)]
169    pub signaling: RawSignalingConfig,
170
171    #[serde(default)]
172    pub ais_endpoint: RawAisEndpointConfig,
173
174    #[serde(default)]
175    pub deployment: RawDeploymentConfig,
176
177    #[serde(default)]
178    pub discovery: RawDiscoveryConfig,
179
180    #[serde(default)]
181    pub storage: RawStorageConfig,
182
183    #[serde(default)]
184    pub webrtc: RawWebRtcConfig,
185    #[serde(default)]
186    pub websocket: RawWebSocketConfig,
187    #[serde(default)]
188    pub observability: RawObservabilityConfig,
189}
190
191/// WebSocket data transport configuration
192///
193/// Configuration example (actr.toml):
194/// ```toml
195/// [system.websocket]
196/// listen_port = 9001
197/// advertised_host = "192.168.1.10"
198/// ```
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct RawWebSocketConfig {
201    /// Port for listening to inbound WebSocket connections (for direct mode)
202    ///
203    /// When configured, the node starts a WebSocket server on this port,
204    /// accepting direct connections from peer nodes.
205    /// When not configured, the node does not listen on any port (relay mode only).
206    #[serde(default)]
207    pub listen_port: Option<u16>,
208
209    /// Externally advertised WebSocket hostname or IP (for signaling registration)
210    ///
211    /// When a node has `listen_port` configured, the signaling server needs an address
212    /// reachable by peer nodes. This field specifies the hostname or IP registered with
213    /// the signaling server, e.g., `"192.168.1.10"` or `"mynode.example.com"`.
214    /// Defaults to `"127.0.0.1"` if not configured (suitable for local testing only).
215    #[serde(default)]
216    pub advertised_host: Option<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220pub struct RawSignalingConfig {
221    #[serde(default)]
222    pub url: Option<String>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, Default)]
226pub struct RawAisEndpointConfig {
227    #[serde(default)]
228    pub url: Option<String>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, Default)]
232pub struct RawDeploymentConfig {
233    #[serde(default)]
234    pub realm_id: Option<u32>,
235
236    /// Realm secret for AIS registration authentication
237    #[serde(default)]
238    pub realm_secret: Option<String>,
239
240    /// AIS (Actor Identity Service) HTTP endpoint, e.g. `"http://ais.example.com:8080"`.
241    #[serde(default)]
242    pub ais_endpoint: Option<String>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, Default)]
246pub struct RawDiscoveryConfig {
247    #[serde(default)]
248    pub visible: Option<bool>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, Default)]
252#[serde(deny_unknown_fields)]
253pub struct RawStorageConfig {
254    #[serde(default)]
255    pub mailbox_path: Option<PathBuf>,
256}
257
258/// WebRTC configuration
259///
260/// Without port range configured, uses default mode (random ports).
261/// With port_range_start/end configured, enables fixed port mode.
262#[derive(Debug, Clone, Serialize, Deserialize, Default)]
263pub struct RawWebRtcConfig {
264    /// STUN server URL list (e.g., ["stun:localhost:3478"])
265    #[serde(default)]
266    pub stun_urls: Vec<String>,
267
268    /// TURN server URL list (e.g., ["turn:localhost:3478"])
269    #[serde(default)]
270    pub turn_urls: Vec<String>,
271
272    /// Whether to force TURN relay (default false)
273    #[serde(default)]
274    pub force_relay: bool,
275
276    /// ICE host candidate acceptance min wait (ms)
277    #[serde(default)]
278    pub ice_host_acceptance_min_wait: Option<u64>,
279
280    /// ICE srflx candidate acceptance min wait (ms)
281    #[serde(default)]
282    pub ice_srflx_acceptance_min_wait: Option<u64>,
283
284    /// ICE prflx candidate acceptance min wait (ms)
285    #[serde(default)]
286    pub ice_prflx_acceptance_min_wait: Option<u64>,
287
288    /// ICE relay candidate acceptance min wait (ms)
289    #[serde(default)]
290    pub ice_relay_acceptance_min_wait: Option<u64>,
291
292    /// UDP port range start (optional, enables fixed port mode when configured)
293    #[serde(default)]
294    pub port_range_start: Option<u16>,
295
296    /// UDP port range end (optional, enables fixed port mode when configured)
297    #[serde(default)]
298    pub port_range_end: Option<u16>,
299
300    /// NAT 1:1 public IP mapping (optional)
301    #[serde(default)]
302    pub public_ips: Vec<String>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, Default)]
306pub struct RawObservabilityConfig {
307    /// Filter level (e.g., "info", "debug", "warn", "info,webrtc=debug").
308    /// Used when RUST_LOG environment variable is not set. Default: "info".
309    #[serde(default)]
310    pub filter_level: Option<String>,
311
312    #[serde(default)]
313    pub tracing_enabled: Option<bool>,
314
315    /// OTLP/Jaeger gRPC endpoint. Default: http://localhost:4317
316    #[serde(default)]
317    pub tracing_endpoint: Option<String>,
318
319    /// Service name reported to the tracing backend. Default: package.name
320    #[serde(default)]
321    pub tracing_service_name: Option<String>,
322}
323
324fn default_edition() -> u32 {
325    1
326}
327
328impl ManifestRawConfig {
329    /// Load raw configuration from file
330    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
331        let content = std::fs::read_to_string(path)?;
332        content.parse()
333    }
334
335    /// Save to file
336    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
337        let content = toml::to_string_pretty(self)?;
338        std::fs::write(path, content)?;
339        Ok(())
340    }
341}
342
343impl FromStr for ManifestRawConfig {
344    type Err = crate::error::ConfigError;
345
346    fn from_str(s: &str) -> Result<Self> {
347        toml::from_str(s).map_err(Into::into)
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_parse_basic_config() {
357        let toml_content = r#"
358edition = 1
359exports = ["proto/test.proto"]
360
361[package]
362name = "test-service"
363manufacturer = "acme"
364
365[dependencies]
366user-service = {}
367
368[scripts]
369run = "cargo run"
370"#;
371
372        let config = ManifestRawConfig::from_str(toml_content).unwrap();
373        assert_eq!(config.edition, 1);
374        assert_eq!(config.package.name, "test-service");
375        assert_eq!(config.exports.len(), 1);
376        assert!(config.dependencies.contains_key("user-service"));
377    }
378
379    #[test]
380    fn test_parse_dependency_with_empty_attributes() {
381        let toml_content = r#"
382[package]
383name = "test"
384manufacturer = "acme"
385[dependencies]
386user-service = {}
387"#;
388        let config = ManifestRawConfig::from_str(toml_content).unwrap();
389        let dep = config.dependencies.get("user-service").unwrap();
390        assert!(matches!(dep, RawDependency::Empty {}));
391    }
392
393    #[test]
394    fn test_parse_dependency_specified() {
395        let toml_content = r#"
396[package]
397name = "test"
398manufacturer = "acme"
399[dependencies]
400shared = { actr_type = "acme:logging-service:1.0.0", service = "LoggingService:abc123", realm = 9999 }
401"#;
402        let config = ManifestRawConfig::from_str(toml_content).unwrap();
403        let dep = config.dependencies.get("shared").unwrap();
404        if let RawDependency::Specified {
405            actr_type,
406            service,
407            realm,
408        } = dep
409        {
410            assert_eq!(actr_type, "acme:logging-service:1.0.0");
411            assert_eq!(service.as_deref(), Some("LoggingService:abc123"));
412            assert_eq!(*realm, Some(9999));
413        } else {
414            panic!("Expected Specified");
415        }
416    }
417
418    #[test]
419    fn test_parse_dependency_specified_no_service() {
420        let toml_content = r#"
421[package]
422name = "test"
423manufacturer = "acme"
424[dependencies]
425shared = { actr_type = "acme:logging-service:1.0.0" }
426"#;
427        let config = ManifestRawConfig::from_str(toml_content).unwrap();
428        let dep = config.dependencies.get("shared").unwrap();
429        if let RawDependency::Specified { service, .. } = dep {
430            assert!(service.is_none());
431        } else {
432            panic!("Expected Specified");
433        }
434    }
435}