actr_cli/core/components/
service_discovery.rs

1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use heck::ToUpperCamelCase;
4use sha2::{Digest, Sha256};
5use std::path::PathBuf;
6use std::time::SystemTime;
7
8use crate::core::{
9    AvailabilityStatus, HealthStatus, MethodDefinition, ProtoFile, ServiceDefinition,
10    ServiceDetails, ServiceDiscovery, ServiceFilter, ServiceInfo,
11};
12
13#[derive(Clone)]
14struct CatalogEntry {
15    info: ServiceInfo,
16    tags: Vec<String>,
17    dependencies: Vec<String>,
18    proto_files: Vec<ProtoFile>,
19}
20
21pub struct NetworkServiceDiscovery {
22    catalog: Vec<CatalogEntry>,
23}
24
25impl NetworkServiceDiscovery {
26    pub fn new() -> Self {
27        let catalog = vec![
28            Self::build_entry(
29                "user-service",
30                "1.0.0",
31                "User profile and authentication service",
32                &["user", "auth"],
33                &[],
34            ),
35            Self::build_entry(
36                "order-service",
37                "1.2.0",
38                "Order management service",
39                &["order", "workflow"],
40                &["actr://user-service/"],
41            ),
42            Self::build_entry(
43                "payment-service",
44                "2.0.0",
45                "Payments and billing service",
46                &["payment", "billing"],
47                &["actr://order-service/"],
48            ),
49        ];
50
51        Self { catalog }
52    }
53
54    fn build_entry(
55        name: &str,
56        version: &str,
57        description: &str,
58        tags: &[&str],
59        dependencies: &[&str],
60    ) -> CatalogEntry {
61        let methods = Self::build_methods(name);
62        let info = ServiceInfo {
63            name: name.to_string(),
64            uri: format!("actr://{name}/"),
65            version: version.to_string(),
66            fingerprint: Self::fingerprint_for(name),
67            description: Some(description.to_string()),
68            methods: methods.clone(),
69        };
70        let proto_files = vec![Self::build_proto_file(name, &methods)];
71
72        CatalogEntry {
73            info,
74            tags: tags.iter().map(|tag| (*tag).to_string()).collect(),
75            dependencies: dependencies.iter().map(|dep| (*dep).to_string()).collect(),
76            proto_files,
77        }
78    }
79
80    fn build_methods(name: &str) -> Vec<MethodDefinition> {
81        let service_name = name.to_upper_camel_case();
82        vec![
83            MethodDefinition {
84                name: format!("Get{service_name}"),
85                input_type: format!("Get{service_name}Request"),
86                output_type: format!("Get{service_name}Response"),
87            },
88            MethodDefinition {
89                name: format!("List{service_name}"),
90                input_type: format!("List{service_name}Request"),
91                output_type: format!("List{service_name}Response"),
92            },
93        ]
94    }
95
96    fn build_proto_file(name: &str, methods: &[MethodDefinition]) -> ProtoFile {
97        let service_name = format!("{}Service", name.to_upper_camel_case());
98        let package_name = name.replace('-', "_");
99        let mut service_methods = String::new();
100        let mut message_defs = String::new();
101
102        for method in methods {
103            service_methods.push_str(&format!(
104                "  rpc {} ({}) returns ({});\n",
105                method.name, method.input_type, method.output_type
106            ));
107            message_defs.push_str(&format!("message {} {{}}\n", method.input_type));
108            message_defs.push_str(&format!("message {} {{}}\n", method.output_type));
109        }
110
111        let content = format!(
112            "syntax = \"proto3\";\n\npackage {package_name};\n\nservice {service_name} {{\n{service_methods}}}\n\n{message_defs}",
113        );
114
115        ProtoFile {
116            name: format!("{name}.proto"),
117            path: PathBuf::from(format!("proto/{name}.proto")),
118            content,
119            services: vec![ServiceDefinition {
120                name: service_name,
121                methods: methods.to_vec(),
122            }],
123        }
124    }
125
126    fn fingerprint_for(name: &str) -> String {
127        let mut hasher = Sha256::new();
128        hasher.update(name.as_bytes());
129        let digest = hasher.finalize();
130        let hex = digest
131            .iter()
132            .map(|b| format!("{b:02x}"))
133            .collect::<String>();
134        format!("sha256:{hex}")
135    }
136
137    fn parse_actr_uri(&self, uri: &str) -> Result<String> {
138        if !uri.starts_with("actr://") {
139            return Err(anyhow!("Invalid actr:// URI: {uri}"));
140        }
141
142        let without_scheme = &uri["actr://".len()..];
143        let name_end = without_scheme
144            .find(|c| ['/', '?'].contains(&c))
145            .unwrap_or(without_scheme.len());
146        let name = without_scheme[..name_end].trim();
147        if name.is_empty() {
148            return Err(anyhow!("Invalid actr:// URI: {uri}"));
149        }
150
151        Ok(name.to_string())
152    }
153
154    fn matches_filter(entry: &CatalogEntry, filter: &ServiceFilter) -> bool {
155        if let Some(pattern) = &filter.name_pattern
156            && !Self::matches_pattern(&entry.info.name, pattern)
157        {
158            return false;
159        }
160
161        if let Some(version_range) = &filter.version_range
162            && entry.info.version != *version_range
163        {
164            return false;
165        }
166
167        if let Some(tags) = &filter.tags {
168            let has_all = tags.iter().all(|tag| entry.tags.iter().any(|t| t == tag));
169            if !has_all {
170                return false;
171            }
172        }
173
174        true
175    }
176
177    fn matches_pattern(value: &str, pattern: &str) -> bool {
178        if pattern == "*" {
179            return true;
180        }
181
182        let segments: Vec<&str> = pattern.split('*').collect();
183        if segments.len() == 1 {
184            return value == pattern;
185        }
186
187        if !pattern.starts_with('*')
188            && let Some(first) = segments.first()
189            && !value.starts_with(first)
190        {
191            return false;
192        }
193
194        if !pattern.ends_with('*')
195            && let Some(last) = segments.last()
196            && !value.ends_with(last)
197        {
198            return false;
199        }
200
201        let mut search_start = 0;
202        let end_limit = if !pattern.ends_with('*') {
203            value
204                .len()
205                .saturating_sub(segments.last().unwrap_or(&"").len())
206        } else {
207            value.len()
208        };
209
210        for (index, segment) in segments.iter().enumerate() {
211            if segment.is_empty() {
212                continue;
213            }
214            if index == 0 && !pattern.starts_with('*') {
215                search_start = segment.len();
216                continue;
217            }
218            if index == segments.len() - 1 && !pattern.ends_with('*') {
219                continue;
220            }
221            if let Some(found) = value[search_start..end_limit].find(segment) {
222                search_start += found + segment.len();
223            } else {
224                return false;
225            }
226        }
227
228        true
229    }
230
231    fn find_entry(&self, name: &str) -> Option<&CatalogEntry> {
232        self.catalog.iter().find(|entry| entry.info.name == name)
233    }
234}
235
236impl Default for NetworkServiceDiscovery {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242#[async_trait]
243impl ServiceDiscovery for NetworkServiceDiscovery {
244    async fn discover_services(&self, filter: Option<&ServiceFilter>) -> Result<Vec<ServiceInfo>> {
245        let services = match filter {
246            Some(filter) => self
247                .catalog
248                .iter()
249                .filter(|entry| Self::matches_filter(entry, filter))
250                .map(|entry| entry.info.clone())
251                .collect(),
252            None => self
253                .catalog
254                .iter()
255                .map(|entry| entry.info.clone())
256                .collect(),
257        };
258        Ok(services)
259    }
260
261    async fn get_service_details(&self, uri: &str) -> Result<ServiceDetails> {
262        let name = self.parse_actr_uri(uri)?;
263        let entry = self
264            .find_entry(&name)
265            .ok_or_else(|| anyhow!("Service not found: {name}"))?;
266
267        Ok(ServiceDetails {
268            info: entry.info.clone(),
269            proto_files: entry.proto_files.clone(),
270            dependencies: entry.dependencies.clone(),
271        })
272    }
273
274    async fn check_service_availability(&self, uri: &str) -> Result<AvailabilityStatus> {
275        let name = self.parse_actr_uri(uri)?;
276        let available = self.find_entry(&name).is_some();
277
278        Ok(AvailabilityStatus {
279            is_available: available,
280            last_seen: available.then(SystemTime::now),
281            health: if available {
282                HealthStatus::Healthy
283            } else {
284                HealthStatus::Unknown
285            },
286        })
287    }
288
289    async fn get_service_proto(&self, uri: &str) -> Result<Vec<ProtoFile>> {
290        let name = self.parse_actr_uri(uri)?;
291        let entry = self
292            .find_entry(&name)
293            .ok_or_else(|| anyhow!("Service not found: {name}"))?;
294        Ok(entry.proto_files.clone())
295    }
296}