1use 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#[derive(Debug, Deserialize)]
12struct ApiResponse<T> {
13 success: bool,
15
16 message: String,
18
19 #[serde(default)]
21 data: Option<T>,
22}
23
24#[derive(Debug, Deserialize, Default)]
26struct RegisterResponse {
27 service_name: String,
29
30 instance_id: String,
32}
33
34#[derive(Debug, Default, Serialize, Deserialize)]
36pub struct RouteOptions {
37 pub timeout_ms: Option<u64>,
39
40 pub retry_count: Option<u32>,
42
43 pub preserve_host_header: Option<bool>,
45}
46
47pub struct LodestoneClient {
49 client: Client,
51
52 base_url: String,
54}
55
56impl LodestoneClient {
57 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 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 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 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 let response = self.client.post(&url)
108 .json(&request)
109 .send()
110 .await
111 .context("Failed to send request")?;
112
113 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 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 pub async fn deregister_service(&self, instance_id: &str) -> Result<()> {
132 debug!("Deregistering service instance {}", instance_id);
133
134 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 let response = self.client.delete(&url)
145 .send()
146 .await
147 .context("Failed to send request")?;
148
149 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 pub async fn add_service_route(
163 &self,
164 path: &str, service_name: &str, options: Option<RouteOptions>
167 ) -> Result<()> {
168 debug!("Adding route {} -> service {}", path, service_name);
169
170 let url = format!("{}/v1/routes/{}", self.base_url, path);
172
173 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 let response = self.client.put(&url)
185 .json(&request)
186 .send()
187 .await
188 .context("Failed to send route request")?;
189
190 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 pub async fn get_services(&self) -> Result<HashMap<String, Service>> {
206 debug!("Fetching all services");
207
208 let url = format!("{}/v1/services", self.base_url);
210
211 let response = self.client.get(&url)
213 .send()
214 .await
215 .context("Failed to send request")?;
216
217 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 if let Some(services) = body.data {
228 Ok(services)
229 } else {
230 Ok(HashMap::new())
231 }
232 }
233
234 pub async fn get_service(&self, name: &str) -> Result<Service> {
236 debug!("Fetching service {}", name);
237
238 let url = format!("{}/v1/services/{}", self.base_url, name);
240
241 let response = self.client.get(&url)
243 .send()
244 .await
245 .context("Failed to send request")?;
246
247 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 if let Some(service) = body.data {
262 Ok(service)
263 } else {
264 Err(anyhow::anyhow!("No service data in response"))
265 }
266 }
267
268 pub async fn discover_by_tag(&self, tag: &str) -> Result<Vec<ServiceInstance>> {
270 debug!("Discovering services with tag {}", tag);
271
272 let services = self.get_services().await?;
274
275 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 pub async fn discover_service(&self, name: &str) -> Result<Vec<ServiceInstance>> {
290 debug!("Discovering healthy instances of service {}", name);
291
292 match self.get_service(name).await {
294 Ok(service) => {
295 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}