use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestRawConfig {
#[serde(default = "default_edition")]
pub edition: u32,
#[serde(default)]
pub inherit: Option<PathBuf>,
#[serde(default)]
pub config_dir: Option<PathBuf>,
pub package: RawPackageConfig,
#[serde(default)]
pub exports: Vec<PathBuf>,
#[serde(default)]
pub dependencies: HashMap<String, RawDependency>,
#[serde(default)]
pub acl: Option<toml::Value>,
#[serde(default)]
pub scripts: HashMap<String, String>,
#[serde(default)]
pub binary: Option<RawBinaryConfig>,
#[serde(default)]
pub build: Option<RawBuildConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawPackageConfig {
pub name: String,
pub manufacturer: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub authors: Option<Vec<String>>,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub signature_algorithm: Option<String>,
#[serde(default)]
pub exports: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawBinaryConfig {
pub path: PathBuf,
#[serde(default)]
pub target: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawBuildConfig {
#[serde(default)]
pub tool: Option<String>,
#[serde(default)]
pub manifest_path: Option<PathBuf>,
#[serde(default)]
pub artifact: Option<String>,
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub no_default_features: bool,
#[serde(default)]
pub post_build: Vec<String>,
}
impl RawPackageConfig {
pub fn into_package_info(self) -> Result<crate::config::PackageInfo> {
Ok(crate::config::PackageInfo {
name: self.name.clone(),
actr_type: actr_protocol::ActrType {
manufacturer: self.manufacturer.clone(),
name: self.name,
version: self.version,
},
description: self.description,
authors: self.authors.unwrap_or_default(),
license: self.license,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RawDependency {
Specified {
#[serde(rename = "actr_type")]
actr_type: String,
#[serde(default)]
service: Option<String>,
#[serde(default)]
realm: Option<u32>,
},
Empty {},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawSystemConfig {
#[serde(default)]
pub signaling: RawSignalingConfig,
#[serde(default)]
pub ais_endpoint: RawAisEndpointConfig,
#[serde(default)]
pub deployment: RawDeploymentConfig,
#[serde(default)]
pub discovery: RawDiscoveryConfig,
#[serde(default)]
pub storage: RawStorageConfig,
#[serde(default)]
pub webrtc: RawWebRtcConfig,
#[serde(default)]
pub websocket: RawWebSocketConfig,
#[serde(default)]
pub observability: RawObservabilityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawWebSocketConfig {
#[serde(default)]
pub listen_port: Option<u16>,
#[serde(default)]
pub advertised_host: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawSignalingConfig {
#[serde(default)]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawAisEndpointConfig {
#[serde(default)]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawDeploymentConfig {
#[serde(default)]
pub realm_id: Option<u32>,
#[serde(default)]
pub realm_secret: Option<String>,
#[serde(default)]
pub ais_endpoint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawDiscoveryConfig {
#[serde(default)]
pub visible: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RawStorageConfig {
#[serde(default)]
pub mailbox_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawWebRtcConfig {
#[serde(default)]
pub stun_urls: Vec<String>,
#[serde(default)]
pub turn_urls: Vec<String>,
#[serde(default)]
pub force_relay: bool,
#[serde(default)]
pub ice_host_acceptance_min_wait: Option<u64>,
#[serde(default)]
pub ice_srflx_acceptance_min_wait: Option<u64>,
#[serde(default)]
pub ice_prflx_acceptance_min_wait: Option<u64>,
#[serde(default)]
pub ice_relay_acceptance_min_wait: Option<u64>,
#[serde(default)]
pub port_range_start: Option<u16>,
#[serde(default)]
pub port_range_end: Option<u16>,
#[serde(default)]
pub public_ips: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RawObservabilityConfig {
#[serde(default)]
pub filter_level: Option<String>,
#[serde(default)]
pub tracing_enabled: Option<bool>,
#[serde(default)]
pub tracing_endpoint: Option<String>,
#[serde(default)]
pub tracing_service_name: Option<String>,
}
fn default_edition() -> u32 {
1
}
impl ManifestRawConfig {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
content.parse()
}
pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
}
impl FromStr for ManifestRawConfig {
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_basic_config() {
let toml_content = r#"
edition = 1
exports = ["proto/test.proto"]
[package]
name = "test-service"
manufacturer = "acme"
[dependencies]
user-service = {}
[scripts]
run = "cargo run"
"#;
let config = ManifestRawConfig::from_str(toml_content).unwrap();
assert_eq!(config.edition, 1);
assert_eq!(config.package.name, "test-service");
assert_eq!(config.exports.len(), 1);
assert!(config.dependencies.contains_key("user-service"));
}
#[test]
fn test_parse_dependency_with_empty_attributes() {
let toml_content = r#"
[package]
name = "test"
manufacturer = "acme"
[dependencies]
user-service = {}
"#;
let config = ManifestRawConfig::from_str(toml_content).unwrap();
let dep = config.dependencies.get("user-service").unwrap();
assert!(matches!(dep, RawDependency::Empty {}));
}
#[test]
fn test_parse_dependency_specified() {
let toml_content = r#"
[package]
name = "test"
manufacturer = "acme"
[dependencies]
shared = { actr_type = "acme:logging-service:1.0.0", service = "LoggingService:abc123", realm = 9999 }
"#;
let config = ManifestRawConfig::from_str(toml_content).unwrap();
let dep = config.dependencies.get("shared").unwrap();
if let RawDependency::Specified {
actr_type,
service,
realm,
} = dep
{
assert_eq!(actr_type, "acme:logging-service:1.0.0");
assert_eq!(service.as_deref(), Some("LoggingService:abc123"));
assert_eq!(*realm, Some(9999));
} else {
panic!("Expected Specified");
}
}
#[test]
fn test_parse_dependency_specified_no_service() {
let toml_content = r#"
[package]
name = "test"
manufacturer = "acme"
[dependencies]
shared = { actr_type = "acme:logging-service:1.0.0" }
"#;
let config = ManifestRawConfig::from_str(toml_content).unwrap();
let dep = config.dependencies.get("shared").unwrap();
if let RawDependency::Specified { service, .. } = dep {
assert!(service.is_none());
} else {
panic!("Expected Specified");
}
}
}