actr_config/
config.rs

1//! Final configuration structures - fully parsed and validated
2
3use actr_protocol::{Acl, ActrType, Realm};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use url::Url;
7
8/// 最终配置(已处理继承、默认值、验证、类型转换)
9/// 注意:没有 edition 字段,edition 只作用于解析阶段
10#[derive(Debug, Clone)]
11pub struct Config {
12    /// 包信息
13    pub package: PackageInfo,
14
15    /// 导出的 proto 文件(已读取内容)
16    pub exports: Vec<ProtoFile>,
17
18    /// 服务依赖(已展开)
19    pub dependencies: Vec<Dependency>,
20
21    /// 信令服务器 URL(已验证)
22    pub signaling_url: Url,
23
24    /// 所属 Realm (Security Realm)
25    pub realm: Realm,
26
27    /// 是否在服务发现中可见
28    pub visible_in_discovery: bool,
29
30    /// 访问控制列表
31    pub acl: Option<Acl>,
32
33    /// Mailbox 数据库路径
34    ///
35    /// - `Some(path)`: 使用持久化 SQLite 数据库
36    /// - `None`: 使用内存模式 (`:memory:`)
37    pub mailbox_path: Option<PathBuf>,
38
39    /// Service tags (e.g., "latest", "stable", "v1.0")
40    pub tags: Vec<String>,
41
42    /// 脚本命令
43    pub scripts: HashMap<String, String>,
44
45    /// WebRTC 配置
46    pub webrtc: WebRtcConfig,
47
48    /// Observability configuration (logging + tracing)
49    pub observability: ObservabilityConfig,
50
51    /// Directory containing the configuration file (Actr.toml)
52    /// Used for resolving relative paths and finding lock files
53    pub config_dir: PathBuf,
54}
55
56/// 包信息
57#[derive(Debug, Clone)]
58pub struct PackageInfo {
59    /// 包名
60    pub name: String,
61
62    /// Actor 类型
63    pub actr_type: ActrType,
64
65    /// 描述
66    pub description: Option<String>,
67
68    /// 作者列表
69    pub authors: Vec<String>,
70
71    /// 许可证
72    pub license: Option<String>,
73}
74
75/// 已解析的 proto 文件(文件级别)
76#[derive(Debug, Clone)]
77pub struct ProtoFile {
78    /// 文件路径(绝对路径)
79    pub path: PathBuf,
80
81    /// 文件内容
82    pub content: String,
83}
84
85/// 已展开的依赖
86#[derive(Debug, Clone)]
87pub struct Dependency {
88    /// 依赖别名(dependencies 中的 key)
89    pub alias: String,
90
91    /// True name of the dependency, provided by the name field in [dependencies].
92    /// If not provided, use alias as name.
93    /// Same as the name field in LockFile.dependency.name.
94    pub name: String,
95
96    /// 所属 Realm
97    pub realm: Realm,
98
99    /// Actor 类型
100    pub actr_type: Option<ActrType>,
101
102    /// 服务指纹
103    pub fingerprint: Option<String>,
104}
105
106/// ICE 传输策略
107#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub enum IceTransportPolicy {
109    /// 使用所有可用候选(默认)
110    #[default]
111    All,
112    /// 仅使用 TURN 中继候选
113    Relay,
114}
115
116/// ICE 服务器配置
117#[derive(Clone, Debug, Default)]
118pub struct IceServer {
119    /// 服务器 URL 列表
120    pub urls: Vec<String>,
121    /// 用户名(TURN 服务器需要)
122    pub username: Option<String>,
123    /// 凭证(TURN 服务器需要)
124    pub credential: Option<String>,
125}
126
127/// WebRTC 配置
128#[derive(Clone, Debug, Default)]
129pub struct WebRtcConfig {
130    /// ICE 服务器列表
131    pub ice_servers: Vec<IceServer>,
132    /// ICE 传输策略(All 或 Relay)
133    pub ice_transport_policy: IceTransportPolicy,
134}
135/// Observability configuration (logging + tracing) resolved from raw config
136#[derive(Debug, Clone)]
137pub struct ObservabilityConfig {
138    /// Filter level (e.g., "info", "debug", "warn", "info,webrtc=debug").
139    /// Used when RUST_LOG environment variable is not set. Default: "info".
140    pub filter_level: String,
141
142    /// Whether to enable distributed tracing
143    pub tracing_enabled: bool,
144
145    /// OTLP/Jaeger gRPC endpoint
146    pub tracing_endpoint: String,
147
148    /// Service name reported to the tracing backend
149    pub tracing_service_name: String,
150}
151
152// ============================================================================
153// Config 辅助方法
154// ============================================================================
155
156impl Config {
157    /// 获取包的 ActrType(用于注册)
158    pub fn actr_type(&self) -> &ActrType {
159        &self.package.actr_type
160    }
161
162    /// 获取所有 proto 文件路径
163    pub fn proto_paths(&self) -> Vec<&PathBuf> {
164        self.exports.iter().map(|p| &p.path).collect()
165    }
166
167    /// 获取所有 proto 内容(用于计算服务指纹)
168    pub fn proto_contents(&self) -> Vec<&str> {
169        self.exports.iter().map(|p| p.content.as_str()).collect()
170    }
171
172    /// 根据别名查找依赖
173    pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
174        self.dependencies.iter().find(|d| d.alias == alias)
175    }
176
177    /// 获取所有跨 Realm 的依赖
178    pub fn cross_realm_dependencies(&self) -> Vec<&Dependency> {
179        self.dependencies
180            .iter()
181            .filter(|d| d.realm.realm_id != self.realm.realm_id)
182            .collect()
183    }
184
185    /// 获取脚本命令
186    pub fn get_script(&self, name: &str) -> Option<&str> {
187        self.scripts.get(name).map(|s| s.as_str())
188    }
189
190    /// 列出所有脚本名称
191    pub fn list_scripts(&self) -> Vec<&str> {
192        self.scripts.keys().map(|s| s.as_str()).collect()
193    }
194
195    /// Calculate ServiceSpec from config
196    ///
197    /// Returns None if no proto files are exported
198    pub fn calculate_service_spec(&self) -> Option<actr_protocol::ServiceSpec> {
199        // If no exports, no ServiceSpec
200        if self.exports.is_empty() {
201            return None;
202        }
203
204        // Convert exports to ProtoFile format for fingerprint calculation
205        let proto_files: Vec<actr_version::ProtoFile> = self
206            .exports
207            .iter()
208            .map(|export| actr_version::ProtoFile {
209                name: export
210                    .path
211                    .file_name()
212                    .and_then(|n| n.to_str())
213                    .unwrap_or("unknown.proto")
214                    .to_string(),
215                content: export.content.clone(),
216                path: export.path.to_str().map(|s| s.to_string()),
217            })
218            .collect();
219
220        // Calculate service fingerprint
221        let fingerprint =
222            actr_version::Fingerprint::calculate_service_semantic_fingerprint(&proto_files).ok()?;
223
224        // Build Protobuf entries
225        let protobufs = self
226            .exports
227            .iter()
228            .map(|export| {
229                // Calculate individual file fingerprint
230                let file_fingerprint =
231                    actr_version::Fingerprint::calculate_proto_semantic_fingerprint(
232                        &export.content,
233                    )
234                    .unwrap_or_else(|_| "error".to_string());
235
236                actr_protocol::service_spec::Protobuf {
237                    package: export
238                        .path
239                        .file_stem()
240                        .and_then(|n| n.to_str())
241                        .unwrap_or("unknown")
242                        .to_string(),
243                    content: export.content.clone(),
244                    fingerprint: file_fingerprint,
245                }
246            })
247            .collect();
248
249        // Get current timestamp
250        let published_at = std::time::SystemTime::now()
251            .duration_since(std::time::UNIX_EPOCH)
252            .ok()?
253            .as_secs() as i64;
254
255        Some(actr_protocol::ServiceSpec {
256            name: self.package.name.clone(),
257            description: self.package.description.clone(),
258            fingerprint,
259            protobufs,
260            published_at: Some(published_at),
261            tags: self.tags.clone(),
262        })
263    }
264}
265
266// ============================================================================
267// PackageInfo 辅助方法
268// ============================================================================
269
270impl PackageInfo {
271    /// 获取 manufacturer(ActrType.manufacturer)
272    pub fn manufacturer(&self) -> &str {
273        &self.actr_type.manufacturer
274    }
275
276    /// 获取 type name(ActrType.name)
277    pub fn type_name(&self) -> &str {
278        &self.actr_type.name
279    }
280}
281
282// ============================================================================
283// Dependency 辅助方法
284// ============================================================================
285
286impl Dependency {
287    /// 是否跨 Realm 依赖
288    pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
289        self.realm.realm_id != self_realm.realm_id
290    }
291
292    /// 检查指纹是否匹配
293    pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
294        self.fingerprint
295            .as_ref()
296            .map(|fp| fp == fingerprint)
297            .unwrap_or(true) // 无指纹要求则总是匹配
298    }
299}
300
301// ============================================================================
302// ProtoFile 辅助方法
303// ============================================================================
304
305impl ProtoFile {
306    /// 获取文件名
307    pub fn file_name(&self) -> Option<&str> {
308        self.path.file_name()?.to_str()
309    }
310
311    /// 获取文件扩展名
312    pub fn extension(&self) -> Option<&str> {
313        self.path.extension()?.to_str()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_config_methods() {
323        let config = Config {
324            package: PackageInfo {
325                name: "test-service".to_string(),
326                actr_type: ActrType {
327                    manufacturer: "acme".to_string(),
328                    name: "test-service".to_string(),
329                },
330                description: None,
331                authors: vec![],
332                license: None,
333            },
334            exports: vec![],
335            dependencies: vec![
336                Dependency {
337                    alias: "user-service".to_string(),
338                    name: "user-service".to_string(),
339                    realm: Realm { realm_id: 1001 },
340                    actr_type: Some(ActrType {
341                        manufacturer: "acme".to_string(),
342                        name: "user-service".to_string(),
343                    }),
344                    fingerprint: Some("service_semantic:abc123...".to_string()),
345                },
346                Dependency {
347                    alias: "shared-logger".to_string(),
348                    name: "shared-logger".to_string(),
349                    realm: Realm { realm_id: 9999 },
350                    actr_type: Some(ActrType {
351                        manufacturer: "common".to_string(),
352                        name: "logging-service".to_string(),
353                    }),
354                    fingerprint: None,
355                },
356            ],
357            signaling_url: Url::parse("ws://localhost:8081").unwrap(),
358            realm: Realm { realm_id: 1001 },
359            visible_in_discovery: true,
360            acl: None,
361            mailbox_path: None,
362            tags: vec![],
363            scripts: HashMap::new(),
364            webrtc: WebRtcConfig::default(),
365            observability: ObservabilityConfig {
366                filter_level: "info".to_string(),
367                tracing_enabled: false,
368                tracing_endpoint: "http://localhost:4317".to_string(),
369                tracing_service_name: "test-service".to_string(),
370            },
371            config_dir: PathBuf::from("."),
372        };
373
374        // 测试依赖查找
375        assert!(config.get_dependency("user-service").is_some());
376        assert!(config.get_dependency("not-exists").is_none());
377
378        // 测试跨 Realm 依赖
379        let cross_realm = config.cross_realm_dependencies();
380        assert_eq!(cross_realm.len(), 1);
381        assert_eq!(cross_realm[0].alias, "shared-logger");
382
383        // 测试指纹匹配
384        let user_dep = config.get_dependency("user-service").unwrap();
385        assert!(user_dep.matches_fingerprint("service_semantic:abc123..."));
386        assert!(!user_dep.matches_fingerprint("service_semantic:different"));
387    }
388}