use actr_protocol::{Acl, ActrType, Realm};
use std::collections::HashMap;
use std::path::PathBuf;
use url::Url;
#[derive(Debug, Clone)]
pub struct ManifestConfig {
pub package: PackageInfo,
pub exports: Vec<ProtoFile>,
pub dependencies: Vec<Dependency>,
pub acl: Option<Acl>,
pub tags: Vec<String>,
pub scripts: HashMap<String, String>,
pub binary: Option<BinaryConfig>,
pub build: Option<BuildConfig>,
pub config_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct RuntimeConfig {
pub package: PackageInfo,
pub signaling_url: Url,
pub realm: Realm,
pub ais_endpoint: String,
pub realm_secret: Option<String>,
pub visible_in_discovery: bool,
pub acl: Option<Acl>,
pub mailbox_path: Option<PathBuf>,
pub scripts: HashMap<String, String>,
pub webrtc: WebRtcConfig,
pub websocket_listen_port: Option<u16>,
pub websocket_advertised_host: Option<String>,
pub observability: ObservabilityConfig,
pub config_dir: PathBuf,
pub trust: Vec<TrustAnchor>,
pub package_path: Option<PathBuf>,
pub web: Option<WebConfig>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TrustAnchor {
Static {
#[serde(default, skip_serializing_if = "Option::is_none")]
pubkey_file: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pubkey_b64: Option<String>,
},
Registry {
endpoint: String,
},
}
#[derive(Debug, Clone)]
pub struct PackageInfo {
pub name: String,
pub actr_type: ActrType,
pub description: Option<String>,
pub authors: Vec<String>,
pub license: Option<String>,
}
#[derive(Debug, Clone)]
pub struct BinaryConfig {
pub path: PathBuf,
pub target: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildTool {
Cargo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildArtifact {
Lib,
Bin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildProfile {
Dev,
Release,
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub tool: BuildTool,
pub manifest_path: PathBuf,
pub artifact: BuildArtifact,
pub target: Option<String>,
pub profile: BuildProfile,
pub features: Vec<String>,
pub no_default_features: bool,
pub post_build: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ProtoFile {
pub path: PathBuf,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub alias: String,
pub realm: Realm,
pub actr_type: Option<ActrType>,
pub service: Option<ServiceRef>,
}
#[derive(Debug, Clone)]
pub struct ServiceRef {
pub name: String,
pub fingerprint: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum IceTransportPolicy {
#[default]
All,
Relay,
}
#[derive(Clone, Debug, Default)]
pub struct IceServer {
pub urls: Vec<String>,
pub username: Option<String>,
pub credential: Option<String>,
}
type UdpPorts = Option<(u16, u16)>;
#[derive(Clone, Debug)]
pub struct WebRtcAdvancedConfig {
pub udp_ports: UdpPorts,
pub public_ips: Vec<String>,
pub ice_host_acceptance_min_wait: u64,
pub ice_srflx_acceptance_min_wait: u64,
pub ice_prflx_acceptance_min_wait: u64,
pub ice_relay_acceptance_min_wait: u64,
}
impl WebRtcAdvancedConfig {
pub fn prefer_answerer(&self) -> bool {
self.udp_ports.is_some() || !self.public_ips.is_empty()
}
}
impl Default for WebRtcAdvancedConfig {
fn default() -> Self {
Self {
udp_ports: UdpPorts::default(),
public_ips: Vec::new(),
ice_host_acceptance_min_wait: 0,
ice_srflx_acceptance_min_wait: 20,
ice_prflx_acceptance_min_wait: 40,
ice_relay_acceptance_min_wait: 100,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct WebRtcConfig {
pub ice_servers: Vec<IceServer>,
pub ice_transport_policy: IceTransportPolicy,
pub advanced: WebRtcAdvancedConfig,
}
#[derive(Debug, Clone)]
pub struct ObservabilityConfig {
pub filter_level: String,
pub tracing_enabled: bool,
pub tracing_endpoint: String,
pub tracing_service_name: String,
}
#[derive(Debug, Clone)]
pub struct WebConfig {
pub port: u16,
pub host: String,
pub static_dir: PathBuf,
pub package_url: Option<String>,
pub runtime_wasm_url: Option<String>,
}
impl ManifestConfig {
pub fn actr_type(&self) -> &ActrType {
&self.package.actr_type
}
pub fn proto_paths(&self) -> Vec<&PathBuf> {
self.exports.iter().map(|p| &p.path).collect()
}
pub fn proto_contents(&self) -> Vec<&str> {
self.exports.iter().map(|p| p.content.as_str()).collect()
}
pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
self.dependencies.iter().find(|d| d.alias == alias)
}
pub fn get_script(&self, name: &str) -> Option<&str> {
self.scripts.get(name).map(|s| s.as_str())
}
pub fn list_scripts(&self) -> Vec<&str> {
self.scripts.keys().map(|s| s.as_str()).collect()
}
}
impl RuntimeConfig {
pub fn actr_type(&self) -> &ActrType {
&self.package.actr_type
}
pub fn cross_realm_dependencies(&self) -> Vec<&Dependency> {
vec![]
}
pub fn get_script(&self, name: &str) -> Option<&str> {
self.scripts.get(name).map(|s| s.as_str())
}
}
impl PackageInfo {
pub fn manufacturer(&self) -> &str {
&self.actr_type.manufacturer
}
pub fn type_name(&self) -> &str {
&self.actr_type.name
}
}
impl BuildProfile {
pub fn as_str(self) -> &'static str {
match self {
Self::Dev => "dev",
Self::Release => "release",
}
}
}
impl Dependency {
pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
self.realm.realm_id != self_realm.realm_id
}
pub fn requires_exact_fingerprint(&self) -> bool {
self.service.is_some()
}
pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
self.service
.as_ref()
.map(|s| s.fingerprint == fingerprint)
.unwrap_or(true)
}
}
impl ProtoFile {
pub fn file_name(&self) -> Option<&str> {
self.path.file_name()?.to_str()
}
pub fn extension(&self) -> Option<&str> {
self.path.extension()?.to_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_config_methods() {
let config = ManifestConfig {
package: PackageInfo {
name: "test-service".to_string(),
actr_type: ActrType {
manufacturer: "acme".to_string(),
name: "test-service".to_string(),
version: "1.0.0".to_string(),
},
description: None,
authors: vec![],
license: None,
},
exports: vec![],
dependencies: vec![
Dependency {
alias: "user-service".to_string(),
realm: Realm { realm_id: 1001 },
actr_type: Some(ActrType {
manufacturer: "acme".to_string(),
name: "user-service".to_string(),
version: "2.1.0".to_string(),
}),
service: Some(ServiceRef {
name: "UserService".to_string(),
fingerprint: "abc123".to_string(),
}),
},
Dependency {
alias: "shared-logger".to_string(),
realm: Realm { realm_id: 9999 },
actr_type: Some(ActrType {
manufacturer: "common".to_string(),
name: "logging-service".to_string(),
version: "1.0.0".to_string(),
}),
service: None,
},
],
acl: None,
tags: vec![],
scripts: HashMap::new(),
binary: None,
build: None,
config_dir: PathBuf::from("."),
};
assert!(config.get_dependency("user-service").is_some());
assert!(config.get_dependency("not-exists").is_none());
let user_dep = config.get_dependency("user-service").unwrap();
assert!(user_dep.matches_fingerprint("abc123"));
assert!(!user_dep.matches_fingerprint("different"));
let logger_dep = config.get_dependency("shared-logger").unwrap();
assert!(logger_dep.matches_fingerprint("any-fingerprint"));
assert!(!logger_dep.requires_exact_fingerprint());
}
#[test]
fn test_runtime_config_methods() {
let config = RuntimeConfig {
package: PackageInfo {
name: "test-service".to_string(),
actr_type: ActrType {
manufacturer: "acme".to_string(),
name: "test-service".to_string(),
version: "1.0.0".to_string(),
},
description: None,
authors: vec![],
license: None,
},
signaling_url: Url::parse("ws://localhost:8081").unwrap(),
realm: Realm { realm_id: 1001 },
ais_endpoint: "http://localhost:8081/ais".to_string(),
realm_secret: None,
visible_in_discovery: true,
acl: None,
mailbox_path: None,
scripts: HashMap::new(),
webrtc: WebRtcConfig::default(),
websocket_listen_port: None,
websocket_advertised_host: None,
observability: ObservabilityConfig {
filter_level: "info".to_string(),
tracing_enabled: false,
tracing_endpoint: "http://localhost:4317".to_string(),
tracing_service_name: "test-service".to_string(),
},
config_dir: PathBuf::from("."),
trust: vec![],
package_path: None,
web: None,
};
assert_eq!(config.actr_type().name, "test-service");
assert!(config.cross_realm_dependencies().is_empty());
}
}