use crate::error::Result;
use crate::raw::{
RawAisEndpointConfig, RawDeploymentConfig, RawDiscoveryConfig, RawObservabilityConfig,
RawSignalingConfig, RawStorageConfig, RawWebRtcConfig, RawWebSocketConfig,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeRawConfig {
#[serde(default = "default_edition")]
pub edition: u32,
#[serde(default)]
pub signaling: RawSignalingConfig,
#[serde(default)]
pub ais_endpoint: RawAisEndpointConfig,
#[serde(default)]
pub deployment: RawDeploymentConfig,
#[serde(default)]
pub discovery: RawDiscoveryConfig,
#[serde(default)]
pub webrtc: RawWebRtcConfig,
#[serde(default)]
pub websocket: RawWebSocketConfig,
#[serde(default)]
pub observability: RawObservabilityConfig,
#[serde(default)]
pub storage: RawStorageConfig,
#[serde(default)]
pub capabilities: Option<RawCapabilitiesConfig>,
#[serde(default)]
pub acl: Option<toml::Value>,
#[serde(default)]
pub scripts: HashMap<String, String>,
#[serde(default)]
pub package: Option<RawPackagePathConfig>,
#[serde(default)]
pub trust: Vec<crate::config::TrustAnchor>,
#[serde(default)]
pub web: Option<RawWebConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawPackagePathConfig {
#[serde(default)]
pub path: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawCapabilitiesConfig {
#[serde(default)]
pub max_concurrent_requests: Option<u32>,
#[serde(default)]
pub version_range: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub tags: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawWebConfig {
#[serde(default = "default_web_port")]
pub port: u16,
#[serde(default = "default_web_host")]
pub host: String,
#[serde(default = "default_web_static_dir")]
pub static_dir: String,
pub package_url: Option<String>,
pub runtime_wasm_url: Option<String>,
}
fn default_web_port() -> u16 {
8080
}
fn default_web_host() -> String {
"0.0.0.0".to_string()
}
fn default_web_static_dir() -> String {
"public".to_string()
}
fn default_edition() -> u32 {
1
}
impl RuntimeRawConfig {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
content.parse()
}
}
impl FromStr for RuntimeRawConfig {
type Err = crate::error::ConfigError;
fn from_str(s: &str) -> Result<Self> {
toml::from_str(s).map_err(Into::into)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_actr_config() {
let toml_content = r#"
edition = 1
[signaling]
url = "ws://localhost:8081/signaling/ws"
[deployment]
realm_id = 1001
"#;
let config = RuntimeRawConfig::from_str(toml_content).unwrap();
assert_eq!(config.edition, 1);
assert_eq!(
config.signaling.url.as_deref(),
Some("ws://localhost:8081/signaling/ws")
);
assert_eq!(config.deployment.realm_id, Some(1001));
}
#[test]
fn test_parse_full_actr_config() {
let toml_content = r#"
edition = 1
[signaling]
url = "ws://localhost:8081/signaling/ws"
[ais_endpoint]
url = "http://localhost:8081/ais"
[deployment]
realm_id = 33554432
realm_secret = "rs_test123"
[discovery]
visible = true
[webrtc]
force_relay = false
stun_urls = ["stun:localhost:3478"]
turn_urls = ["turn:localhost:3478"]
[websocket]
listen_port = 9001
advertised_host = "127.0.0.1"
[observability]
filter_level = "info"
tracing_enabled = false
[capabilities]
max_concurrent_requests = 100
version_range = "1.0.0-2.0.0"
region = "cn-beijing"
[capabilities.tags]
env = "prod"
tier = "premium"
[acl]
[[acl.rules]]
permission = "allow"
type = "acme:EchoService:1.0.0"
[scripts]
dev = "cargo run"
test = "cargo test"
"#;
let config = RuntimeRawConfig::from_str(toml_content).unwrap();
assert_eq!(config.edition, 1);
assert_eq!(
config.ais_endpoint.url.as_deref(),
Some("http://localhost:8081/ais")
);
assert_eq!(
config.deployment.realm_secret.as_deref(),
Some("rs_test123")
);
assert_eq!(config.discovery.visible, Some(true));
assert!(!config.webrtc.force_relay);
assert_eq!(config.webrtc.stun_urls.len(), 1);
assert_eq!(config.websocket.listen_port, Some(9001));
assert_eq!(config.observability.filter_level.as_deref(), Some("info"));
let caps = config.capabilities.unwrap();
assert_eq!(caps.max_concurrent_requests, Some(100));
assert_eq!(caps.region.as_deref(), Some("cn-beijing"));
assert_eq!(
caps.tags
.as_ref()
.and_then(|t| t.get("env"))
.map(|s| s.as_str()),
Some("prod")
);
assert!(config.acl.is_some());
assert_eq!(
config.scripts.get("dev").map(|s| s.as_str()),
Some("cargo run")
);
}
#[test]
fn test_parse_empty_actr_config() {
let toml_content = "edition = 1\n";
let config = RuntimeRawConfig::from_str(toml_content).unwrap();
assert_eq!(config.edition, 1);
assert!(config.signaling.url.is_none());
assert!(config.capabilities.is_none());
}
#[test]
fn test_parse_actr_config_with_package_path() {
let toml_content = r#"
edition = 1
[signaling]
url = "ws://localhost:8081/signaling/ws"
[deployment]
realm_id = 1001
[package]
path = "dist/service.actr"
"#;
let config = RuntimeRawConfig::from_str(toml_content).unwrap();
assert_eq!(config.edition, 1);
assert!(config.package.is_some());
let package = config.package.unwrap();
assert_eq!(
package.path.as_ref().map(|p| p.to_str().unwrap()),
Some("dist/service.actr")
);
}
#[test]
fn test_reject_runtime_hyper_data_dir() {
let toml_content = r#"
edition = 1
[signaling]
url = "ws://localhost:8081/signaling/ws"
[deployment]
realm_id = 1001
[storage]
hyper_data_dir = ".hyper"
"#;
let error = RuntimeRawConfig::from_str(toml_content).unwrap_err();
assert!(
error.to_string().contains("unknown field `hyper_data_dir`"),
"unexpected error: {error}"
);
}
}