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
52#[derive(Debug, Clone)]
54pub struct PackageInfo {
55 pub name: String,
57
58 pub actr_type: ActrType,
60
61 pub description: Option<String>,
63
64 pub authors: Vec<String>,
66
67 pub license: Option<String>,
69}
70
71#[derive(Debug, Clone)]
73pub struct ProtoFile {
74 pub path: PathBuf,
76
77 pub content: String,
79}
80
81#[derive(Debug, Clone)]
83pub struct Dependency {
84 pub alias: String,
86
87 pub realm: Realm,
89
90 pub actr_type: ActrType,
92
93 pub fingerprint: Option<String>,
95}
96
97#[derive(Clone, Debug, Default, PartialEq, Eq)]
99pub enum IceTransportPolicy {
100 #[default]
102 All,
103 Relay,
105}
106
107#[derive(Clone, Debug, Default)]
109pub struct IceServer {
110 pub urls: Vec<String>,
112 pub username: Option<String>,
114 pub credential: Option<String>,
116}
117
118#[derive(Clone, Debug, Default)]
120pub struct WebRtcConfig {
121 pub ice_servers: Vec<IceServer>,
123 pub ice_transport_policy: IceTransportPolicy,
125}
126#[derive(Debug, Clone)]
128pub struct ObservabilityConfig {
129 pub filter_level: String,
132
133 pub tracing_enabled: bool,
135
136 pub tracing_endpoint: String,
138
139 pub tracing_service_name: String,
141}
142
143impl Config {
148 pub fn actr_type(&self) -> &ActrType {
150 &self.package.actr_type
151 }
152
153 pub fn proto_paths(&self) -> Vec<&PathBuf> {
155 self.exports.iter().map(|p| &p.path).collect()
156 }
157
158 pub fn proto_contents(&self) -> Vec<&str> {
160 self.exports.iter().map(|p| p.content.as_str()).collect()
161 }
162
163 pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
165 self.dependencies.iter().find(|d| d.alias == alias)
166 }
167
168 pub fn find_dependencies_by_type(&self, actr_type: &ActrType) -> Vec<&Dependency> {
170 self.dependencies
171 .iter()
172 .filter(|d| &d.actr_type == actr_type)
173 .collect()
174 }
175
176 pub fn cross_realm_dependencies(&self) -> Vec<&Dependency> {
178 self.dependencies
179 .iter()
180 .filter(|d| d.realm.realm_id != self.realm.realm_id)
181 .collect()
182 }
183
184 pub fn get_script(&self, name: &str) -> Option<&str> {
186 self.scripts.get(name).map(|s| s.as_str())
187 }
188
189 pub fn list_scripts(&self) -> Vec<&str> {
191 self.scripts.keys().map(|s| s.as_str()).collect()
192 }
193
194 pub fn calculate_service_spec(&self) -> Option<actr_protocol::ServiceSpec> {
198 if self.exports.is_empty() {
200 return None;
201 }
202
203 let proto_files: Vec<actr_version::ProtoFile> = self
205 .exports
206 .iter()
207 .map(|export| actr_version::ProtoFile {
208 name: export
209 .path
210 .file_name()
211 .and_then(|n| n.to_str())
212 .unwrap_or("unknown.proto")
213 .to_string(),
214 content: export.content.clone(),
215 path: export.path.to_str().map(|s| s.to_string()),
216 })
217 .collect();
218
219 let fingerprint =
221 actr_version::Fingerprint::calculate_service_semantic_fingerprint(&proto_files).ok()?;
222
223 let protobufs = self
225 .exports
226 .iter()
227 .map(|export| {
228 let file_fingerprint =
230 actr_version::Fingerprint::calculate_proto_semantic_fingerprint(
231 &export.content,
232 )
233 .unwrap_or_else(|_| "error".to_string());
234
235 actr_protocol::service_spec::Protobuf {
236 package: export
237 .path
238 .file_stem()
239 .and_then(|n| n.to_str())
240 .unwrap_or("unknown")
241 .to_string(),
242 content: export.content.clone(),
243 fingerprint: file_fingerprint,
244 }
245 })
246 .collect();
247
248 let published_at = std::time::SystemTime::now()
250 .duration_since(std::time::UNIX_EPOCH)
251 .ok()?
252 .as_secs() as i64;
253
254 Some(actr_protocol::ServiceSpec {
255 description: self.package.description.clone(),
256 fingerprint,
257 protobufs,
258 published_at: Some(published_at),
259 tags: self.tags.clone(),
260 })
261 }
262}
263
264impl PackageInfo {
269 pub fn manufacturer(&self) -> &str {
271 &self.actr_type.manufacturer
272 }
273
274 pub fn type_name(&self) -> &str {
276 &self.actr_type.name
277 }
278}
279
280impl Dependency {
285 pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
287 self.realm.realm_id != self_realm.realm_id
288 }
289
290 pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
292 self.fingerprint
293 .as_ref()
294 .map(|fp| fp == fingerprint)
295 .unwrap_or(true) }
297}
298
299impl ProtoFile {
304 pub fn file_name(&self) -> Option<&str> {
306 self.path.file_name()?.to_str()
307 }
308
309 pub fn extension(&self) -> Option<&str> {
311 self.path.extension()?.to_str()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_config_methods() {
321 let config = Config {
322 package: PackageInfo {
323 name: "test-service".to_string(),
324 actr_type: ActrType {
325 manufacturer: "acme".to_string(),
326 name: "test-service".to_string(),
327 },
328 description: None,
329 authors: vec![],
330 license: None,
331 },
332 exports: vec![],
333 dependencies: vec![
334 Dependency {
335 alias: "user-service".to_string(),
336 realm: Realm { realm_id: 1001 },
337 actr_type: ActrType {
338 manufacturer: "acme".to_string(),
339 name: "user-service".to_string(),
340 },
341 fingerprint: Some("service_semantic:abc123...".to_string()),
342 },
343 Dependency {
344 alias: "shared-logger".to_string(),
345 realm: Realm { realm_id: 9999 },
346 actr_type: ActrType {
347 manufacturer: "common".to_string(),
348 name: "logging-service".to_string(),
349 },
350 fingerprint: None,
351 },
352 ],
353 signaling_url: Url::parse("ws://localhost:8081").unwrap(),
354 realm: Realm { realm_id: 1001 },
355 visible_in_discovery: true,
356 acl: None,
357 mailbox_path: None,
358 tags: vec![],
359 scripts: HashMap::new(),
360 webrtc: WebRtcConfig::default(),
361 observability: ObservabilityConfig {
362 filter_level: "info".to_string(),
363 tracing_enabled: false,
364 tracing_endpoint: "http://localhost:4317".to_string(),
365 tracing_service_name: "test-service".to_string(),
366 },
367 };
368
369 assert!(config.get_dependency("user-service").is_some());
371 assert!(config.get_dependency("not-exists").is_none());
372
373 let cross_realm = config.cross_realm_dependencies();
375 assert_eq!(cross_realm.len(), 1);
376 assert_eq!(cross_realm[0].alias, "shared-logger");
377
378 let user_dep = config.get_dependency("user-service").unwrap();
380 assert!(user_dep.matches_fingerprint("service_semantic:abc123..."));
381 assert!(!user_dep.matches_fingerprint("service_semantic:different"));
382 }
383}