1use crate::runbeam_api::types::{ApiError, AuthorizeResponse, RunbeamError};
2use serde::Serialize;
3
4#[derive(Debug, Clone)]
9pub struct RunbeamClient {
10 base_url: String,
12 client: reqwest::Client,
14}
15
16#[derive(Debug, Serialize)]
18struct AuthorizeRequest {
19 token: String,
21 gateway_code: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 machine_public_key: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 metadata: Option<Vec<String>>,
29}
30
31impl RunbeamClient {
32 pub fn new(base_url: impl Into<String>) -> Self {
46 let base_url = base_url.into();
47 tracing::debug!("Creating RunbeamClient with base URL: {}", base_url);
48
49 Self {
50 base_url,
51 client: reqwest::Client::new(),
52 }
53 }
54
55 pub async fn authorize_gateway(
112 &self,
113 user_token: impl Into<String>,
114 gateway_code: impl Into<String>,
115 machine_public_key: Option<String>,
116 metadata: Option<Vec<String>>,
117 ) -> Result<AuthorizeResponse, RunbeamError> {
118 let user_token = user_token.into();
119 let gateway_code = gateway_code.into();
120
121 tracing::info!(
122 "Authorizing gateway with Runbeam Cloud: gateway_code={}",
123 gateway_code
124 );
125
126 let url = format!("{}/api/harmony/authorize", self.base_url);
128
129 let payload = AuthorizeRequest {
131 token: user_token.clone(),
132 gateway_code: gateway_code.clone(),
133 machine_public_key,
134 metadata,
135 };
136
137 tracing::debug!("Sending authorization request to: {}", url);
138
139 let response = self
141 .client
142 .post(&url)
143 .header("Authorization", format!("Bearer {}", user_token))
144 .header("Content-Type", "application/json")
145 .json(&payload)
146 .send()
147 .await
148 .map_err(|e| {
149 tracing::error!("Failed to send authorization request: {}", e);
150 ApiError::from(e)
151 })?;
152
153 let status = response.status();
154 tracing::debug!("Received response with status: {}", status);
155
156 if !status.is_success() {
158 let error_body = response
159 .text()
160 .await
161 .unwrap_or_else(|_| "Unknown error".to_string());
162
163 tracing::error!(
164 "Authorization failed: HTTP {} - {}",
165 status.as_u16(),
166 error_body
167 );
168
169 return Err(RunbeamError::Api(ApiError::Http {
170 status: status.as_u16(),
171 message: error_body,
172 }));
173 }
174
175 let auth_response: AuthorizeResponse = response.json().await.map_err(|e| {
177 tracing::error!("Failed to parse authorization response: {}", e);
178 ApiError::Parse(format!("Failed to parse response JSON: {}", e))
179 })?;
180
181 tracing::info!(
182 "Gateway authorized successfully: gateway_id={}, expires_at={}",
183 auth_response.gateway.id,
184 auth_response.expires_at
185 );
186
187 tracing::debug!(
188 "Machine token length: {}",
189 auth_response.machine_token.len()
190 );
191 tracing::debug!("Gateway abilities: {:?}", auth_response.abilities);
192
193 Ok(auth_response)
194 }
195
196 pub fn base_url(&self) -> &str {
198 &self.base_url
199 }
200
201 pub async fn list_gateways(
214 &self,
215 token: impl Into<String>,
216 ) -> Result<
217 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Gateway>,
218 RunbeamError,
219 > {
220 let url = format!("{}/api/gateways", self.base_url);
221
222 let response = self
223 .client
224 .get(&url)
225 .header("Authorization", format!("Bearer {}", token.into()))
226 .send()
227 .await
228 .map_err(ApiError::from)?;
229
230 if !response.status().is_success() {
231 let status = response.status();
232 let error_body = response
233 .text()
234 .await
235 .unwrap_or_else(|_| "Unknown error".to_string());
236 return Err(RunbeamError::Api(ApiError::Http {
237 status: status.as_u16(),
238 message: error_body,
239 }));
240 }
241
242 response.json().await.map_err(|e| {
243 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
244 })
245 }
246
247 pub async fn get_gateway(
259 &self,
260 token: impl Into<String>,
261 gateway_id: impl Into<String>,
262 ) -> Result<
263 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Gateway>,
264 RunbeamError,
265 > {
266 let url = format!("{}/api/gateways/{}", self.base_url, gateway_id.into());
267
268 let response = self
269 .client
270 .get(&url)
271 .header("Authorization", format!("Bearer {}", token.into()))
272 .send()
273 .await
274 .map_err(ApiError::from)?;
275
276 if !response.status().is_success() {
277 let status = response.status();
278 let error_body = response
279 .text()
280 .await
281 .unwrap_or_else(|_| "Unknown error".to_string());
282 return Err(RunbeamError::Api(ApiError::Http {
283 status: status.as_u16(),
284 message: error_body,
285 }));
286 }
287
288 response.json().await.map_err(|e| {
289 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
290 })
291 }
292
293 pub async fn list_services(
306 &self,
307 token: impl Into<String>,
308 ) -> Result<
309 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Service>,
310 RunbeamError,
311 > {
312 let url = format!("{}/api/services", self.base_url);
313
314 let response = self
315 .client
316 .get(&url)
317 .header("Authorization", format!("Bearer {}", token.into()))
318 .send()
319 .await
320 .map_err(ApiError::from)?;
321
322 if !response.status().is_success() {
323 let status = response.status();
324 let error_body = response
325 .text()
326 .await
327 .unwrap_or_else(|_| "Unknown error".to_string());
328 return Err(RunbeamError::Api(ApiError::Http {
329 status: status.as_u16(),
330 message: error_body,
331 }));
332 }
333
334 response.json().await.map_err(|e| {
335 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
336 })
337 }
338
339 pub async fn get_service(
351 &self,
352 token: impl Into<String>,
353 service_id: impl Into<String>,
354 ) -> Result<
355 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Service>,
356 RunbeamError,
357 > {
358 let url = format!("{}/api/services/{}", self.base_url, service_id.into());
359
360 let response = self
361 .client
362 .get(&url)
363 .header("Authorization", format!("Bearer {}", token.into()))
364 .send()
365 .await
366 .map_err(ApiError::from)?;
367
368 if !response.status().is_success() {
369 let status = response.status();
370 let error_body = response
371 .text()
372 .await
373 .unwrap_or_else(|_| "Unknown error".to_string());
374 return Err(RunbeamError::Api(ApiError::Http {
375 status: status.as_u16(),
376 message: error_body,
377 }));
378 }
379
380 response.json().await.map_err(|e| {
381 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
382 })
383 }
384
385 pub async fn list_endpoints(
396 &self,
397 token: impl Into<String>,
398 ) -> Result<
399 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Endpoint>,
400 RunbeamError,
401 > {
402 let url = format!("{}/api/endpoints", self.base_url);
403
404 let response = self
405 .client
406 .get(&url)
407 .header("Authorization", format!("Bearer {}", token.into()))
408 .send()
409 .await
410 .map_err(ApiError::from)?;
411
412 if !response.status().is_success() {
413 let status = response.status();
414 let error_body = response
415 .text()
416 .await
417 .unwrap_or_else(|_| "Unknown error".to_string());
418 return Err(RunbeamError::Api(ApiError::Http {
419 status: status.as_u16(),
420 message: error_body,
421 }));
422 }
423
424 response.json().await.map_err(|e| {
425 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
426 })
427 }
428
429 pub async fn list_backends(
440 &self,
441 token: impl Into<String>,
442 ) -> Result<
443 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Backend>,
444 RunbeamError,
445 > {
446 let url = format!("{}/api/backends", self.base_url);
447
448 let response = self
449 .client
450 .get(&url)
451 .header("Authorization", format!("Bearer {}", token.into()))
452 .send()
453 .await
454 .map_err(ApiError::from)?;
455
456 if !response.status().is_success() {
457 let status = response.status();
458 let error_body = response
459 .text()
460 .await
461 .unwrap_or_else(|_| "Unknown error".to_string());
462 return Err(RunbeamError::Api(ApiError::Http {
463 status: status.as_u16(),
464 message: error_body,
465 }));
466 }
467
468 response.json().await.map_err(|e| {
469 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
470 })
471 }
472
473 pub async fn list_pipelines(
484 &self,
485 token: impl Into<String>,
486 ) -> Result<
487 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Pipeline>,
488 RunbeamError,
489 > {
490 let url = format!("{}/api/pipelines", self.base_url);
491
492 let response = self
493 .client
494 .get(&url)
495 .header("Authorization", format!("Bearer {}", token.into()))
496 .send()
497 .await
498 .map_err(ApiError::from)?;
499
500 if !response.status().is_success() {
501 let status = response.status();
502 let error_body = response
503 .text()
504 .await
505 .unwrap_or_else(|_| "Unknown error".to_string());
506 return Err(RunbeamError::Api(ApiError::Http {
507 status: status.as_u16(),
508 message: error_body,
509 }));
510 }
511
512 response.json().await.map_err(|e| {
513 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
514 })
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn test_client_creation() {
524 let client = RunbeamClient::new("http://example.com");
525 assert_eq!(client.base_url(), "http://example.com");
526 }
527
528 #[test]
529 fn test_client_creation_with_string() {
530 let base_url = String::from("http://example.com");
531 let client = RunbeamClient::new(base_url);
532 assert_eq!(client.base_url(), "http://example.com");
533 }
534
535 #[test]
536 fn test_authorize_request_serialization() {
537 let request = AuthorizeRequest {
538 token: "test_token".to_string(),
539 gateway_code: "gw123".to_string(),
540 machine_public_key: Some("pubkey123".to_string()),
541 metadata: None,
542 };
543
544 let json = serde_json::to_string(&request).unwrap();
545 assert!(json.contains("\"token\":\"test_token\""));
546 assert!(json.contains("\"gateway_code\":\"gw123\""));
547 assert!(json.contains("\"machine_public_key\":\"pubkey123\""));
548 }
549
550 #[test]
551 fn test_authorize_request_serialization_without_optional_fields() {
552 let request = AuthorizeRequest {
553 token: "test_token".to_string(),
554 gateway_code: "gw123".to_string(),
555 machine_public_key: None,
556 metadata: None,
557 };
558
559 let json = serde_json::to_string(&request).unwrap();
560 assert!(json.contains("\"token\":\"test_token\""));
561 assert!(json.contains("\"gateway_code\":\"gw123\""));
562 assert!(!json.contains("machine_public_key"));
564 assert!(!json.contains("metadata"));
565 }
566}