1use actr_protocol::{Acl, ActrType, Realm};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use url::Url;
7
8#[derive(Debug, Clone)]
11pub struct Config {
12 pub package: PackageInfo,
14
15 pub exports: Vec<ProtoFile>,
17
18 pub dependencies: Vec<Dependency>,
20
21 pub signaling_url: Url,
23
24 pub realm: Realm,
26
27 pub visible_in_discovery: bool,
29
30 pub acl: Option<Acl>,
32
33 pub mailbox_path: Option<PathBuf>,
38
39 pub tags: Vec<String>,
41
42 pub scripts: HashMap<String, String>,
44
45 pub webrtc: WebRtcConfig,
47
48 pub observability: ObservabilityConfig,
50
51 pub config_dir: PathBuf,
54}
55
56#[derive(Debug, Clone)]
58pub struct PackageInfo {
59 pub name: String,
61
62 pub actr_type: ActrType,
64
65 pub description: Option<String>,
67
68 pub authors: Vec<String>,
70
71 pub license: Option<String>,
73}
74
75#[derive(Debug, Clone)]
77pub struct ProtoFile {
78 pub path: PathBuf,
80
81 pub content: String,
83}
84
85#[derive(Debug, Clone)]
87pub struct Dependency {
88 pub alias: String,
90
91 pub name: String,
95
96 pub realm: Realm,
98
99 pub actr_type: Option<ActrType>,
101
102 pub fingerprint: Option<String>,
104}
105
106#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub enum IceTransportPolicy {
109 #[default]
111 All,
112 Relay,
114}
115
116#[derive(Clone, Debug, Default)]
118pub struct IceServer {
119 pub urls: Vec<String>,
121 pub username: Option<String>,
123 pub credential: Option<String>,
125}
126
127#[derive(Clone, Debug, Default)]
129pub struct WebRtcConfig {
130 pub ice_servers: Vec<IceServer>,
132 pub ice_transport_policy: IceTransportPolicy,
134}
135#[derive(Debug, Clone)]
137pub struct ObservabilityConfig {
138 pub filter_level: String,
141
142 pub tracing_enabled: bool,
144
145 pub tracing_endpoint: String,
147
148 pub tracing_service_name: String,
150}
151
152impl Config {
157 pub fn actr_type(&self) -> &ActrType {
159 &self.package.actr_type
160 }
161
162 pub fn proto_paths(&self) -> Vec<&PathBuf> {
164 self.exports.iter().map(|p| &p.path).collect()
165 }
166
167 pub fn proto_contents(&self) -> Vec<&str> {
169 self.exports.iter().map(|p| p.content.as_str()).collect()
170 }
171
172 pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
174 self.dependencies.iter().find(|d| d.alias == alias)
175 }
176
177 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 pub fn get_script(&self, name: &str) -> Option<&str> {
187 self.scripts.get(name).map(|s| s.as_str())
188 }
189
190 pub fn list_scripts(&self) -> Vec<&str> {
192 self.scripts.keys().map(|s| s.as_str()).collect()
193 }
194
195 pub fn calculate_service_spec(&self) -> Option<actr_protocol::ServiceSpec> {
199 if self.exports.is_empty() {
201 return None;
202 }
203
204 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 let fingerprint =
222 actr_version::Fingerprint::calculate_service_semantic_fingerprint(&proto_files).ok()?;
223
224 let protobufs = self
226 .exports
227 .iter()
228 .map(|export| {
229 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 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
266impl PackageInfo {
271 pub fn manufacturer(&self) -> &str {
273 &self.actr_type.manufacturer
274 }
275
276 pub fn type_name(&self) -> &str {
278 &self.actr_type.name
279 }
280}
281
282impl Dependency {
287 pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
289 self.realm.realm_id != self_realm.realm_id
290 }
291
292 pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
294 self.fingerprint
295 .as_ref()
296 .map(|fp| fp == fingerprint)
297 .unwrap_or(true) }
299}
300
301impl ProtoFile {
306 pub fn file_name(&self) -> Option<&str> {
308 self.path.file_name()?.to_str()
309 }
310
311 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 assert!(config.get_dependency("user-service").is_some());
376 assert!(config.get_dependency("not-exists").is_none());
377
378 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 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}