Lodestone/
client.rs

1// src/client.rs
2use crate::service::{Service, ServiceInstance, ServiceHealth};
3use anyhow::{Result, Context};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7use reqwest::Client;
8use tracing::debug;
9
10/// Standard API response
11#[derive(Debug, Deserialize)]
12struct ApiResponse<T> {
13    /// Success flag
14    success: bool,
15    
16    /// Response message
17    message: String,
18    
19    /// Response data
20    #[serde(default)]
21    data: Option<T>,
22}
23
24/// Service registration response
25#[derive(Debug, Deserialize, Default)]
26struct RegisterResponse {
27    /// Service name
28    service_name: String,
29    
30    /// Instance ID
31    instance_id: String,
32}
33
34/// Options for configuring a route
35#[derive(Debug, Default, Serialize, Deserialize)]
36pub struct RouteOptions {
37    /// Request timeout in milliseconds
38    pub timeout_ms: Option<u64>,
39    
40    /// Number of retry attempts
41    pub retry_count: Option<u32>,
42    
43    /// Preserve the original host header
44    pub preserve_host_header: Option<bool>,
45}
46
47/// Client for interacting with Lodestone
48pub struct LodestoneClient {
49    /// HTTP client
50    client: Client,
51    
52    /// Base URL
53    base_url: String,
54}
55
56impl LodestoneClient {
57    /// Create a new Lodestone client
58    pub fn new(base_url: &str) -> Self {
59        let client = Client::builder()
60            .timeout(Duration::from_secs(10))
61            .build()
62            .expect("Failed to create HTTP client");
63        
64        Self {
65            client,
66            base_url: base_url.trim_end_matches('/').to_string(),
67        }
68    }
69    
70    /// Register a service
71    pub async fn register_service(
72        &self,
73        name: &str,
74        address: &str,
75        tags: Vec<String>,
76    ) -> Result<String> {
77        debug!("Registering service {} at {}", name, address);
78        
79        // Parse address into host and port
80        let parts: Vec<&str> = address.split(':').collect();
81        if parts.len() != 2 {
82            return Err(anyhow::anyhow!("Invalid address format (expected host:port)"));
83        }
84        
85        let host = parts[0];
86        let port = parts[1].parse::<u16>()
87            .context("Invalid port number")?;
88        
89        // Prepare request
90        let url = format!("{}/v1/services", self.base_url);
91        
92        let protocol = if address.starts_with("https://") {
93            "https"
94        } else {
95            "http"
96        };
97        
98        let request = serde_json::json!({
99            "name": name,
100            "host": host,
101            "port": port,
102            "protocol": protocol,
103            "tags": tags,
104        });
105        
106        // Send request
107        let response = self.client.post(&url)
108            .json(&request)
109            .send()
110            .await
111            .context("Failed to send request")?;
112        
113        // Parse response
114        let body: ApiResponse<RegisterResponse> = response.json()
115            .await
116            .context("Failed to parse response")?;
117        
118        if !body.success {
119            return Err(anyhow::anyhow!("API error: {}", body.message));
120        }
121        
122        // Extract instance ID
123        if let Some(data) = body.data {
124            Ok(data.instance_id)
125        } else {
126            Err(anyhow::anyhow!("No instance ID in response"))
127        }
128    }
129    
130    /// Deregister a service
131    pub async fn deregister_service(&self, instance_id: &str) -> Result<()> {
132        debug!("Deregistering service instance {}", instance_id);
133        
134        // Prepare request
135        let parts: Vec<&str> = instance_id.split('-').collect();
136        if parts.len() < 2 {
137            return Err(anyhow::anyhow!("Invalid instance ID format"));
138        }
139        
140        let service_name = parts[0];
141        let url = format!("{}/v1/services/{}/{}", self.base_url, service_name, instance_id);
142        
143        // Send request
144        let response = self.client.delete(&url)
145            .send()
146            .await
147            .context("Failed to send request")?;
148        
149        // Parse response
150        let body: ApiResponse<()> = response.json()
151            .await
152            .context("Failed to parse response")?;
153        
154        if !body.success {
155            return Err(anyhow::anyhow!("API error: {}", body.message));
156        }
157        
158        Ok(())
159    }
160    
161    /// Add a route that proxies to a service in the registry
162    pub async fn add_service_route(
163        &self, 
164        path: &str,     // Local path to expose
165        service_name: &str, // Name of service in registry to proxy to
166        options: Option<RouteOptions>
167    ) -> Result<()> {
168        debug!("Adding route {} -> service {}", path, service_name);
169        
170        // Prepare request
171        let url = format!("{}/v1/routes/{}", self.base_url, path);
172        
173        // Default options if not provided
174        let opts = options.unwrap_or_default();
175        
176        let request = serde_json::json!({
177            "upstream": format!("service://{}", service_name),
178            "timeout_ms": opts.timeout_ms.unwrap_or(30000),
179            "retry_count": opts.retry_count.unwrap_or(3),
180            "preserve_host_header": opts.preserve_host_header.unwrap_or(true),
181        });
182        
183        // Send request
184        let response = self.client.put(&url)
185            .json(&request)
186            .send()
187            .await
188            .context("Failed to send route request")?;
189        
190        // Parse response
191        let body: ApiResponse<()> = response.json()
192            .await
193            .context("Failed to parse response")?;
194        
195        if !body.success {
196            return Err(anyhow::anyhow!("API error: {}", body.message));
197        }
198        
199        Ok(())
200    }
201    
202    // Rest of the previous implementation remains the same...
203    
204    /// Get all services
205    pub async fn get_services(&self) -> Result<HashMap<String, Service>> {
206        debug!("Fetching all services");
207        
208        // Prepare request
209        let url = format!("{}/v1/services", self.base_url);
210        
211        // Send request
212        let response = self.client.get(&url)
213            .send()
214            .await
215            .context("Failed to send request")?;
216        
217        // Parse response
218        let body: ApiResponse<HashMap<String, Service>> = response.json()
219            .await
220            .context("Failed to parse response")?;
221        
222        if !body.success {
223            return Err(anyhow::anyhow!("API error: {}", body.message));
224        }
225        
226        // Extract services
227        if let Some(services) = body.data {
228            Ok(services)
229        } else {
230            Ok(HashMap::new())
231        }
232    }
233    
234    /// Get a specific service
235    pub async fn get_service(&self, name: &str) -> Result<Service> {
236        debug!("Fetching service {}", name);
237        
238        // Prepare request
239        let url = format!("{}/v1/services/{}", self.base_url, name);
240        
241        // Send request
242        let response = self.client.get(&url)
243            .send()
244            .await
245            .context("Failed to send request")?;
246        
247        // Parse response
248        if response.status().is_client_error() {
249            return Err(anyhow::anyhow!("Service not found"));
250        }
251        
252        let body: ApiResponse<Service> = response.json()
253            .await
254            .context("Failed to parse response")?;
255        
256        if !body.success {
257            return Err(anyhow::anyhow!("API error: {}", body.message));
258        }
259        
260        // Extract service
261        if let Some(service) = body.data {
262            Ok(service)
263        } else {
264            Err(anyhow::anyhow!("No service data in response"))
265        }
266    }
267    
268    /// Discover services with a specific tag
269    pub async fn discover_by_tag(&self, tag: &str) -> Result<Vec<ServiceInstance>> {
270        debug!("Discovering services with tag {}", tag);
271        
272        // Get all services first
273        let services = self.get_services().await?;
274        
275        // Filter by tag
276        let mut instances = Vec::new();
277        for (_, service) in services {
278            for instance in service.instances {
279                if instance.tags.contains(&tag.to_string()) && instance.health == ServiceHealth::Healthy {
280                    instances.push(instance);
281                }
282            }
283        }
284        
285        Ok(instances)
286    }
287    
288    /// Discover healthy instances of a specific service
289    pub async fn discover_service(&self, name: &str) -> Result<Vec<ServiceInstance>> {
290        debug!("Discovering healthy instances of service {}", name);
291        
292        // Try to get the specific service
293        match self.get_service(name).await {
294            Ok(service) => {
295                // Filter for healthy instances
296                let instances: Vec<ServiceInstance> = service.instances
297                    .into_iter()
298                    .filter(|i| i.health == ServiceHealth::Healthy)
299                    .collect();
300                
301                Ok(instances)
302            }
303            Err(_) => Ok(Vec::new()),
304        }
305    }
306}