1use crate::runbeam_api::types::{
2 ApiError, AuthorizeResponse, RunbeamError, StoreConfigRequest, StoreConfigResponse,
3};
4use serde::Serialize;
5
6#[derive(Debug, Clone)]
11pub struct RunbeamClient {
12 base_url: String,
14 client: reqwest::Client,
16}
17
18#[derive(Debug, Serialize)]
20struct AuthorizeRequest {
21 token: String,
23 gateway_code: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 machine_public_key: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 metadata: Option<Vec<String>>,
31}
32
33impl RunbeamClient {
34 pub fn new(base_url: impl Into<String>) -> Self {
48 let base_url = base_url.into();
49 tracing::debug!("Creating RunbeamClient with base URL: {}", base_url);
50
51 Self {
52 base_url,
53 client: reqwest::Client::new(),
54 }
55 }
56
57 pub async fn authorize_gateway(
114 &self,
115 user_token: impl Into<String>,
116 gateway_code: impl Into<String>,
117 machine_public_key: Option<String>,
118 metadata: Option<Vec<String>>,
119 ) -> Result<AuthorizeResponse, RunbeamError> {
120 let user_token = user_token.into();
121 let gateway_code = gateway_code.into();
122
123 tracing::info!(
124 "Authorizing gateway with Runbeam Cloud: gateway_code={}",
125 gateway_code
126 );
127
128 let url = format!("{}/harmony/authorize", self.base_url);
130
131 let payload = AuthorizeRequest {
133 token: user_token.clone(),
134 gateway_code: gateway_code.clone(),
135 machine_public_key,
136 metadata,
137 };
138
139 tracing::debug!("Sending authorization request to: {}", url);
140
141 let response = self
143 .client
144 .post(&url)
145 .header("Authorization", format!("Bearer {}", user_token))
146 .header("Content-Type", "application/json")
147 .json(&payload)
148 .send()
149 .await
150 .map_err(|e| {
151 tracing::error!("Failed to send authorization request: {}", e);
152 ApiError::from(e)
153 })?;
154
155 let status = response.status();
156 tracing::debug!("Received response with status: {}", status);
157
158 if !status.is_success() {
160 let error_body = response
161 .text()
162 .await
163 .unwrap_or_else(|_| "Unknown error".to_string());
164
165 tracing::error!(
166 "Authorization failed: HTTP {} - {}",
167 status.as_u16(),
168 error_body
169 );
170
171 return Err(RunbeamError::Api(ApiError::Http {
172 status: status.as_u16(),
173 message: error_body,
174 }));
175 }
176
177 let auth_response: AuthorizeResponse = response.json().await.map_err(|e| {
179 tracing::error!("Failed to parse authorization response: {}", e);
180 ApiError::Parse(format!("Failed to parse response JSON: {}", e))
181 })?;
182
183 tracing::info!(
184 "Gateway authorized successfully: gateway_id={}, expires_at={}",
185 auth_response.gateway.id,
186 auth_response.expires_at
187 );
188
189 tracing::debug!(
190 "Machine token length: {}",
191 auth_response.machine_token.len()
192 );
193 tracing::debug!("Gateway abilities: {:?}", auth_response.abilities);
194
195 Ok(auth_response)
196 }
197
198 pub fn base_url(&self) -> &str {
200 &self.base_url
201 }
202
203 pub async fn list_changes(
230 &self,
231 token: impl Into<String>,
232 ) -> Result<
233 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Change>,
234 RunbeamError,
235 > {
236 let url = format!("{}/harmony/changes", self.base_url);
237
238 tracing::debug!("Listing all changes from: {}", url);
239
240 let response = self
241 .client
242 .get(&url)
243 .header("Authorization", format!("Bearer {}", token.into()))
244 .send()
245 .await
246 .map_err(ApiError::from)?;
247
248 if !response.status().is_success() {
249 let status = response.status();
250 let error_body = response
251 .text()
252 .await
253 .unwrap_or_else(|_| "Unknown error".to_string());
254 tracing::error!("Failed to list changes: HTTP {} - {}", status, error_body);
255 return Err(RunbeamError::Api(ApiError::Http {
256 status: status.as_u16(),
257 message: error_body,
258 }));
259 }
260
261 response.json().await.map_err(|e| {
262 tracing::error!("Failed to parse changes response: {}", e);
263 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
264 })
265 }
266
267 pub async fn list_changes_for_gateway(
298 &self,
299 token: impl Into<String>,
300 gateway_id: impl Into<String>,
301 ) -> Result<
302 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Change>,
303 RunbeamError,
304 > {
305 let gateway_id = gateway_id.into();
306 let url = format!("{}/harmony/changes/{}", self.base_url, gateway_id);
307
308 tracing::debug!("Listing changes for gateway {} from: {}", gateway_id, url);
309
310 let response = self
311 .client
312 .get(&url)
313 .header("Authorization", format!("Bearer {}", token.into()))
314 .send()
315 .await
316 .map_err(ApiError::from)?;
317
318 if !response.status().is_success() {
319 let status = response.status();
320 let error_body = response
321 .text()
322 .await
323 .unwrap_or_else(|_| "Unknown error".to_string());
324 tracing::error!(
325 "Failed to list changes for gateway {}: HTTP {} - {}",
326 gateway_id,
327 status,
328 error_body
329 );
330 return Err(RunbeamError::Api(ApiError::Http {
331 status: status.as_u16(),
332 message: error_body,
333 }));
334 }
335
336 let response_text = response.text().await.map_err(|e| {
337 tracing::error!("Failed to read response body: {}", e);
338 RunbeamError::Api(ApiError::Parse(format!("Failed to read response: {}", e)))
339 })?;
340
341 serde_json::from_str(&response_text).map_err(|e| {
342 tracing::error!(
343 "Failed to parse changes response: {} - Response body: {}",
344 e,
345 response_text
346 );
347 RunbeamError::Api(ApiError::Parse(format!(
348 "Failed to parse response: {} - Body: {}",
349 e, response_text
350 )))
351 })
352 }
353
354 pub async fn get_change(
384 &self,
385 token: impl Into<String>,
386 change_id: impl Into<String>,
387 ) -> Result<
388 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Change>,
389 RunbeamError,
390 > {
391 let change_id = change_id.into();
392 let url = format!("{}/harmony/change/{}", self.base_url, change_id);
393
394 tracing::debug!("Getting change {} from: {}", change_id, url);
395
396 let response = self
397 .client
398 .get(&url)
399 .header("Authorization", format!("Bearer {}", token.into()))
400 .send()
401 .await
402 .map_err(ApiError::from)?;
403
404 if !response.status().is_success() {
405 let status = response.status();
406 let error_body = response
407 .text()
408 .await
409 .unwrap_or_else(|_| "Unknown error".to_string());
410 tracing::error!("Failed to get change: HTTP {} - {}", status, error_body);
411 return Err(RunbeamError::Api(ApiError::Http {
412 status: status.as_u16(),
413 message: error_body,
414 }));
415 }
416
417 response.json().await.map_err(|e| {
418 tracing::error!("Failed to parse change response: {}", e);
419 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
420 })
421 }
422
423 pub async fn list_gateways(
436 &self,
437 token: impl Into<String>,
438 ) -> Result<
439 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Gateway>,
440 RunbeamError,
441 > {
442 let url = format!("{}/gateways", self.base_url);
443
444 let response = self
445 .client
446 .get(&url)
447 .header("Authorization", format!("Bearer {}", token.into()))
448 .send()
449 .await
450 .map_err(ApiError::from)?;
451
452 if !response.status().is_success() {
453 let status = response.status();
454 let error_body = response
455 .text()
456 .await
457 .unwrap_or_else(|_| "Unknown error".to_string());
458 return Err(RunbeamError::Api(ApiError::Http {
459 status: status.as_u16(),
460 message: error_body,
461 }));
462 }
463
464 response.json().await.map_err(|e| {
465 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
466 })
467 }
468
469 pub async fn get_gateway(
481 &self,
482 token: impl Into<String>,
483 gateway_id: impl Into<String>,
484 ) -> Result<
485 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Gateway>,
486 RunbeamError,
487 > {
488 let url = format!("{}/gateways/{}", self.base_url, gateway_id.into());
489
490 let response = self
491 .client
492 .get(&url)
493 .header("Authorization", format!("Bearer {}", token.into()))
494 .send()
495 .await
496 .map_err(ApiError::from)?;
497
498 if !response.status().is_success() {
499 let status = response.status();
500 let error_body = response
501 .text()
502 .await
503 .unwrap_or_else(|_| "Unknown error".to_string());
504 return Err(RunbeamError::Api(ApiError::Http {
505 status: status.as_u16(),
506 message: error_body,
507 }));
508 }
509
510 response.json().await.map_err(|e| {
511 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
512 })
513 }
514
515 pub async fn list_services(
528 &self,
529 token: impl Into<String>,
530 ) -> Result<
531 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Service>,
532 RunbeamError,
533 > {
534 let url = format!("{}/api/services", self.base_url);
535
536 let response = self
537 .client
538 .get(&url)
539 .header("Authorization", format!("Bearer {}", token.into()))
540 .send()
541 .await
542 .map_err(ApiError::from)?;
543
544 if !response.status().is_success() {
545 let status = response.status();
546 let error_body = response
547 .text()
548 .await
549 .unwrap_or_else(|_| "Unknown error".to_string());
550 return Err(RunbeamError::Api(ApiError::Http {
551 status: status.as_u16(),
552 message: error_body,
553 }));
554 }
555
556 response.json().await.map_err(|e| {
557 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
558 })
559 }
560
561 pub async fn get_service(
573 &self,
574 token: impl Into<String>,
575 service_id: impl Into<String>,
576 ) -> Result<
577 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Service>,
578 RunbeamError,
579 > {
580 let url = format!("{}/api/services/{}", self.base_url, service_id.into());
581
582 let response = self
583 .client
584 .get(&url)
585 .header("Authorization", format!("Bearer {}", token.into()))
586 .send()
587 .await
588 .map_err(ApiError::from)?;
589
590 if !response.status().is_success() {
591 let status = response.status();
592 let error_body = response
593 .text()
594 .await
595 .unwrap_or_else(|_| "Unknown error".to_string());
596 return Err(RunbeamError::Api(ApiError::Http {
597 status: status.as_u16(),
598 message: error_body,
599 }));
600 }
601
602 response.json().await.map_err(|e| {
603 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
604 })
605 }
606
607 pub async fn list_endpoints(
618 &self,
619 token: impl Into<String>,
620 ) -> Result<
621 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Endpoint>,
622 RunbeamError,
623 > {
624 let url = format!("{}/api/endpoints", self.base_url);
625
626 let response = self
627 .client
628 .get(&url)
629 .header("Authorization", format!("Bearer {}", token.into()))
630 .send()
631 .await
632 .map_err(ApiError::from)?;
633
634 if !response.status().is_success() {
635 let status = response.status();
636 let error_body = response
637 .text()
638 .await
639 .unwrap_or_else(|_| "Unknown error".to_string());
640 return Err(RunbeamError::Api(ApiError::Http {
641 status: status.as_u16(),
642 message: error_body,
643 }));
644 }
645
646 response.json().await.map_err(|e| {
647 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
648 })
649 }
650
651 pub async fn list_backends(
662 &self,
663 token: impl Into<String>,
664 ) -> Result<
665 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Backend>,
666 RunbeamError,
667 > {
668 let url = format!("{}/api/backends", self.base_url);
669
670 let response = self
671 .client
672 .get(&url)
673 .header("Authorization", format!("Bearer {}", token.into()))
674 .send()
675 .await
676 .map_err(ApiError::from)?;
677
678 if !response.status().is_success() {
679 let status = response.status();
680 let error_body = response
681 .text()
682 .await
683 .unwrap_or_else(|_| "Unknown error".to_string());
684 return Err(RunbeamError::Api(ApiError::Http {
685 status: status.as_u16(),
686 message: error_body,
687 }));
688 }
689
690 response.json().await.map_err(|e| {
691 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
692 })
693 }
694
695 pub async fn list_pipelines(
706 &self,
707 token: impl Into<String>,
708 ) -> Result<
709 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Pipeline>,
710 RunbeamError,
711 > {
712 let url = format!("{}/api/pipelines", self.base_url);
713
714 let response = self
715 .client
716 .get(&url)
717 .header("Authorization", format!("Bearer {}", token.into()))
718 .send()
719 .await
720 .map_err(ApiError::from)?;
721
722 if !response.status().is_success() {
723 let status = response.status();
724 let error_body = response
725 .text()
726 .await
727 .unwrap_or_else(|_| "Unknown error".to_string());
728 return Err(RunbeamError::Api(ApiError::Http {
729 status: status.as_u16(),
730 message: error_body,
731 }));
732 }
733
734 response.json().await.map_err(|e| {
735 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
736 })
737 }
738
739 pub async fn get_transform(
774 &self,
775 token: impl Into<String>,
776 transform_id: impl Into<String>,
777 ) -> Result<
778 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Transform>,
779 RunbeamError,
780 > {
781 let transform_id = transform_id.into();
782 let url = format!("{}/api/transforms/{}", self.base_url, transform_id);
783
784 tracing::debug!("Getting transform {} from: {}", transform_id, url);
785
786 let response = self
787 .client
788 .get(&url)
789 .header("Authorization", format!("Bearer {}", token.into()))
790 .send()
791 .await
792 .map_err(ApiError::from)?;
793
794 if !response.status().is_success() {
795 let status = response.status();
796 let error_body = response
797 .text()
798 .await
799 .unwrap_or_else(|_| "Unknown error".to_string());
800 tracing::error!("Failed to get transform: HTTP {} - {}", status, error_body);
801 return Err(RunbeamError::Api(ApiError::Http {
802 status: status.as_u16(),
803 message: error_body,
804 }));
805 }
806
807 response.json().await.map_err(|e| {
808 tracing::error!("Failed to parse transform response: {}", e);
809 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
810 })
811 }
812
813 pub async fn get_base_url(
841 &self,
842 token: impl Into<String>,
843 ) -> Result<crate::runbeam_api::resources::BaseUrlResponse, RunbeamError> {
844 let token = token.into();
845 let candidates = [
847 format!("{}/api/harmony/base-url", self.base_url),
848 format!("{}/harmony/base-url", self.base_url),
849 ];
850
851 let mut last_err: Option<RunbeamError> = None;
852 for url in candidates {
853 tracing::debug!("Getting base URL from: {}", url);
854 let resp = self
855 .client
856 .get(&url)
857 .header("Authorization", format!("Bearer {}", token))
858 .send()
859 .await;
860
861 let response = match resp {
862 Ok(r) => r,
863 Err(e) => {
864 last_err = Some(ApiError::from(e).into());
865 continue;
866 }
867 };
868
869 if !response.status().is_success() {
870 let status = response.status();
871 let error_body = response
872 .text()
873 .await
874 .unwrap_or_else(|_| "Unknown error".to_string());
875 tracing::warn!(
876 "Base URL discovery attempt failed: HTTP {} - {} (url: {})",
877 status,
878 error_body,
879 url
880 );
881 last_err = Some(RunbeamError::Api(ApiError::Http {
882 status: status.as_u16(),
883 message: error_body,
884 }));
885 continue;
886 }
887
888 let parsed = response.json().await.map_err(|e| {
889 tracing::warn!("Failed to parse base URL response from {}: {}", url, e);
890 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
891 });
892 if parsed.is_ok() {
893 return parsed;
894 } else {
895 last_err = Some(parsed.err().unwrap());
896 }
897 }
898
899 Err(last_err.unwrap_or_else(|| {
900 RunbeamError::Api(ApiError::Request(
901 "Base URL discovery failed for all candidates".to_string(),
902 ))
903 }))
904 }
905
906 pub async fn discover_base_url(&self, token: impl Into<String>) -> Result<Self, RunbeamError> {
908 let resp = self.get_base_url(token).await?;
909 let discovered = resp.full_url.unwrap_or(resp.base_url);
910 tracing::info!("Discovered Runbeam API base URL: {}", discovered);
911 Ok(Self::new(discovered))
912 }
913
914 pub async fn acknowledge_changes(
942 &self,
943 token: impl Into<String>,
944 change_ids: Vec<String>,
945 ) -> Result<crate::runbeam_api::resources::AcknowledgeChangesResponse, RunbeamError> {
946 let url = format!("{}/harmony/changes/acknowledge", self.base_url);
947
948 tracing::info!("Acknowledging {} changes", change_ids.len());
949 tracing::debug!("Change IDs: {:?}", change_ids);
950
951 let payload = crate::runbeam_api::resources::AcknowledgeChangesRequest { change_ids };
952
953 let response = self
954 .client
955 .post(&url)
956 .header("Authorization", format!("Bearer {}", token.into()))
957 .header("Content-Type", "application/json")
958 .json(&payload)
959 .send()
960 .await
961 .map_err(ApiError::from)?;
962
963 if !response.status().is_success() {
964 let status = response.status();
965 let error_body = response
966 .text()
967 .await
968 .unwrap_or_else(|_| "Unknown error".to_string());
969 tracing::error!(
970 "Failed to acknowledge changes: HTTP {} - {}",
971 status,
972 error_body
973 );
974 return Err(RunbeamError::Api(ApiError::Http {
975 status: status.as_u16(),
976 message: error_body,
977 }));
978 }
979
980 response.json().await.map_err(|e| {
981 tracing::error!("Failed to parse acknowledge response: {}", e);
982 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
983 })
984 }
985
986 pub async fn mark_change_applied(
1012 &self,
1013 token: impl Into<String>,
1014 change_id: impl Into<String>,
1015 ) -> Result<crate::runbeam_api::resources::ChangeAppliedResponse, RunbeamError> {
1016 let change_id = change_id.into();
1017 let url = format!("{}/harmony/change/{}/applied", self.base_url, change_id);
1018
1019 tracing::info!("Marking change {} as applied", change_id);
1020
1021 let response = self
1022 .client
1023 .post(&url)
1024 .header("Authorization", format!("Bearer {}", token.into()))
1025 .send()
1026 .await
1027 .map_err(ApiError::from)?;
1028
1029 if !response.status().is_success() {
1030 let status = response.status();
1031 let error_body = response
1032 .text()
1033 .await
1034 .unwrap_or_else(|_| "Unknown error".to_string());
1035 tracing::error!(
1036 "Failed to mark change as applied: HTTP {} - {}",
1037 status,
1038 error_body
1039 );
1040 return Err(RunbeamError::Api(ApiError::Http {
1041 status: status.as_u16(),
1042 message: error_body,
1043 }));
1044 }
1045
1046 response.json().await.map_err(|e| {
1047 tracing::error!("Failed to parse applied response: {}", e);
1048 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1049 })
1050 }
1051
1052 pub async fn mark_change_failed(
1085 &self,
1086 token: impl Into<String>,
1087 change_id: impl Into<String>,
1088 error: String,
1089 details: Option<Vec<String>>,
1090 ) -> Result<crate::runbeam_api::resources::ChangeFailedResponse, RunbeamError> {
1091 let change_id = change_id.into();
1092 let url = format!("{}/harmony/change/{}/failed", self.base_url, change_id);
1093
1094 tracing::warn!("Marking change {} as failed: {}", change_id, error);
1095 if let Some(ref details) = details {
1096 tracing::debug!("Failure details: {:?}", details);
1097 }
1098
1099 let payload = crate::runbeam_api::resources::ChangeFailedRequest { error, details };
1100
1101 let response = self
1102 .client
1103 .post(&url)
1104 .header("Authorization", format!("Bearer {}", token.into()))
1105 .header("Content-Type", "application/json")
1106 .json(&payload)
1107 .send()
1108 .await
1109 .map_err(ApiError::from)?;
1110
1111 if !response.status().is_success() {
1112 let status = response.status();
1113 let error_body = response
1114 .text()
1115 .await
1116 .unwrap_or_else(|_| "Unknown error".to_string());
1117 tracing::error!(
1118 "Failed to mark change as failed: HTTP {} - {}",
1119 status,
1120 error_body
1121 );
1122 return Err(RunbeamError::Api(ApiError::Http {
1123 status: status.as_u16(),
1124 message: error_body,
1125 }));
1126 }
1127
1128 response.json().await.map_err(|e| {
1129 tracing::error!("Failed to parse failed response: {}", e);
1130 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1131 })
1132 }
1133
1134 pub async fn store_config(
1209 &self,
1210 token: impl Into<String>,
1211 config_type: impl Into<String>,
1212 id: Option<String>,
1213 config: impl Into<String>,
1214 ) -> Result<StoreConfigResponse, RunbeamError> {
1215 let config_type = config_type.into();
1216 let config = config.into();
1217 let url = format!("{}/harmony/update", self.base_url);
1218
1219 tracing::info!(
1220 "Storing {} configuration to Runbeam Cloud (id: {:?})",
1221 config_type,
1222 id
1223 );
1224 tracing::debug!("Configuration length: {} bytes", config.len());
1225
1226 let payload = StoreConfigRequest {
1227 config_type: config_type.clone(),
1228 id: id.clone(),
1229 config,
1230 };
1231
1232 let response = self
1233 .client
1234 .post(&url)
1235 .header("Authorization", format!("Bearer {}", token.into()))
1236 .header("Content-Type", "application/json")
1237 .json(&payload)
1238 .send()
1239 .await
1240 .map_err(|e| {
1241 tracing::error!("Failed to send store config request: {}", e);
1242 ApiError::from(e)
1243 })?;
1244
1245 let status = response.status();
1246 tracing::debug!("Received response with status: {}", status);
1247
1248 if !status.is_success() {
1250 let error_body = response
1251 .text()
1252 .await
1253 .unwrap_or_else(|_| "Unknown error".to_string());
1254
1255 tracing::error!(
1256 "Store config failed: HTTP {} - {}",
1257 status.as_u16(),
1258 error_body
1259 );
1260
1261 return Err(RunbeamError::Api(ApiError::Http {
1262 status: status.as_u16(),
1263 message: error_body,
1264 }));
1265 }
1266
1267 let body_text = response.text().await.map_err(|e| {
1269 tracing::error!("Failed to read response body: {}", e);
1270 ApiError::Network(format!("Failed to read response body: {}", e))
1271 })?;
1272
1273 let response_data =
1274 serde_json::from_str::<StoreConfigResponse>(&body_text).map_err(|e| {
1275 tracing::error!("Failed to parse store config response: {}", e);
1276 tracing::error!("Response body was: {}", body_text);
1277 ApiError::Parse(format!("Failed to parse response: {}", e))
1278 })?;
1279
1280 tracing::info!(
1281 "Configuration stored successfully: type={}, id={:?}, action={}",
1282 config_type,
1283 id,
1284 response_data.data.action
1285 );
1286
1287 Ok(response_data)
1288 }
1289
1290 pub async fn request_mesh_token(
1332 &self,
1333 token: impl Into<String>,
1334 mesh_id: impl Into<String>,
1335 destination_url: impl Into<String>,
1336 ) -> Result<crate::runbeam_api::resources::MeshTokenResponse, RunbeamError> {
1337 let mesh_id = mesh_id.into();
1338 let destination_url = destination_url.into();
1339 let url = format!("{}/harmony/mesh/token", self.base_url);
1340
1341 tracing::debug!(
1342 "Requesting mesh token: mesh_id={}, destination={}",
1343 mesh_id,
1344 destination_url
1345 );
1346
1347 let payload = crate::runbeam_api::resources::MeshTokenRequest {
1348 mesh_id: mesh_id.clone(),
1349 destination_url: destination_url.clone(),
1350 };
1351
1352 let response = self
1353 .client
1354 .post(&url)
1355 .header("Authorization", format!("Bearer {}", token.into()))
1356 .header("Content-Type", "application/json")
1357 .json(&payload)
1358 .send()
1359 .await
1360 .map_err(|e| {
1361 tracing::error!("Failed to send mesh token request: {}", e);
1362 ApiError::from(e)
1363 })?;
1364
1365 let status = response.status();
1366 tracing::debug!("Received response with status: {}", status);
1367
1368 if !status.is_success() {
1369 let error_body = response
1370 .text()
1371 .await
1372 .unwrap_or_else(|_| "Unknown error".to_string());
1373
1374 tracing::error!(
1375 "Mesh token request failed: HTTP {} - {}",
1376 status.as_u16(),
1377 error_body
1378 );
1379
1380 return Err(RunbeamError::Api(ApiError::Http {
1381 status: status.as_u16(),
1382 message: error_body,
1383 }));
1384 }
1385
1386 let token_response: crate::runbeam_api::resources::MeshTokenResponse =
1387 response.json().await.map_err(|e| {
1388 tracing::error!("Failed to parse mesh token response: {}", e);
1389 ApiError::Parse(format!("Failed to parse response: {}", e))
1390 })?;
1391
1392 tracing::info!(
1393 "Mesh token obtained: mesh_id={}, expires_at={}",
1394 token_response.mesh_id,
1395 token_response.expires_at
1396 );
1397
1398 Ok(token_response)
1399 }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404 use super::*;
1405
1406 #[test]
1407 fn test_client_creation() {
1408 let client = RunbeamClient::new("http://example.com");
1409 assert_eq!(client.base_url(), "http://example.com");
1410 }
1411
1412 #[test]
1413 fn test_client_creation_with_string() {
1414 let base_url = String::from("http://example.com");
1415 let client = RunbeamClient::new(base_url);
1416 assert_eq!(client.base_url(), "http://example.com");
1417 }
1418
1419 #[test]
1420 fn test_authorize_request_serialization() {
1421 let request = AuthorizeRequest {
1422 token: "test_token".to_string(),
1423 gateway_code: "gw123".to_string(),
1424 machine_public_key: Some("pubkey123".to_string()),
1425 metadata: None,
1426 };
1427
1428 let json = serde_json::to_string(&request).unwrap();
1429 assert!(json.contains("\"token\":\"test_token\""));
1430 assert!(json.contains("\"gateway_code\":\"gw123\""));
1431 assert!(json.contains("\"machine_public_key\":\"pubkey123\""));
1432 }
1433
1434 #[test]
1435 fn test_authorize_request_serialization_without_optional_fields() {
1436 let request = AuthorizeRequest {
1437 token: "test_token".to_string(),
1438 gateway_code: "gw123".to_string(),
1439 machine_public_key: None,
1440 metadata: None,
1441 };
1442
1443 let json = serde_json::to_string(&request).unwrap();
1444 assert!(json.contains("\"token\":\"test_token\""));
1445 assert!(json.contains("\"gateway_code\":\"gw123\""));
1446 assert!(!json.contains("machine_public_key"));
1448 assert!(!json.contains("metadata"));
1449 }
1450
1451 #[test]
1452 fn test_change_serialization() {
1453 use crate::runbeam_api::resources::Change;
1454
1455 let change_metadata = Change {
1457 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1458 status: Some("pending".to_string()),
1459 resource_type: "gateway".to_string(),
1460 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1461 pipeline_id: None,
1462 toml_config: None,
1463 metadata: None,
1464 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1465 acknowledged_at: None,
1466 applied_at: None,
1467 failed_at: None,
1468 error_message: None,
1469 error_details: None,
1470 };
1471
1472 let json = serde_json::to_string(&change_metadata).unwrap();
1473 assert!(json.contains("\"id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1474 assert!(json.contains("\"gateway_id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1475 assert!(json.contains("\"type\":\"gateway\""));
1476
1477 let deserialized: Change = serde_json::from_str(&json).unwrap();
1479 assert_eq!(deserialized.id, "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX");
1480 assert_eq!(deserialized.status, Some("pending".to_string()));
1481 assert_eq!(deserialized.resource_type, "gateway");
1482
1483 let change_detail = Change {
1485 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1486 status: Some("applied".to_string()),
1487 resource_type: "gateway".to_string(),
1488 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1489 pipeline_id: None,
1490 toml_config: Some("[proxy]\nname = \"test\"".to_string()),
1491 metadata: Some(serde_json::json!({"gateway_name": "test-gateway"})),
1492 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1493 acknowledged_at: Some("2025-01-07T01:00:05+00:00".to_string()),
1494 applied_at: Some("2025-01-07T01:00:10+00:00".to_string()),
1495 failed_at: None,
1496 error_message: None,
1497 error_details: None,
1498 };
1499
1500 let json = serde_json::to_string(&change_detail).unwrap();
1501 assert!(json.contains("toml_config"));
1502 assert!(json.contains("acknowledged_at"));
1503 assert!(json.contains("applied_at"));
1504
1505 let deserialized: Change = serde_json::from_str(&json).unwrap();
1507 assert!(deserialized.toml_config.is_some());
1508 assert!(deserialized.acknowledged_at.is_some());
1509 assert!(deserialized.applied_at.is_some());
1510 }
1511
1512 #[test]
1513 fn test_acknowledge_changes_request_serialization() {
1514 use crate::runbeam_api::resources::AcknowledgeChangesRequest;
1515
1516 let request = AcknowledgeChangesRequest {
1517 change_ids: vec![
1518 "change-1".to_string(),
1519 "change-2".to_string(),
1520 "change-3".to_string(),
1521 ],
1522 };
1523
1524 let json = serde_json::to_string(&request).unwrap();
1525 assert!(json.contains("\"change_ids\""));
1526 assert!(json.contains("\"change-1\""));
1527 assert!(json.contains("\"change-2\""));
1528 assert!(json.contains("\"change-3\""));
1529
1530 let deserialized: AcknowledgeChangesRequest = serde_json::from_str(&json).unwrap();
1532 assert_eq!(deserialized.change_ids.len(), 3);
1533 assert_eq!(deserialized.change_ids[0], "change-1");
1534 }
1535
1536 #[test]
1537 fn test_change_failed_request_serialization() {
1538 use crate::runbeam_api::resources::ChangeFailedRequest;
1539
1540 let request_with_details = ChangeFailedRequest {
1542 error: "Configuration parse error".to_string(),
1543 details: Some(vec![
1544 "Invalid JSON at line 42".to_string(),
1545 "Missing required field 'name'".to_string(),
1546 ]),
1547 };
1548
1549 let json = serde_json::to_string(&request_with_details).unwrap();
1550 assert!(json.contains("\"error\":\"Configuration parse error\""));
1551 assert!(json.contains("\"details\""));
1552 assert!(json.contains("Invalid JSON at line 42"));
1553
1554 let request_without_details = ChangeFailedRequest {
1556 error: "Unknown error".to_string(),
1557 details: None,
1558 };
1559
1560 let json = serde_json::to_string(&request_without_details).unwrap();
1561 assert!(json.contains("\"error\":\"Unknown error\""));
1562 assert!(!json.contains("\"details\"")); let deserialized: ChangeFailedRequest =
1566 serde_json::from_str(&serde_json::to_string(&request_with_details).unwrap()).unwrap();
1567 assert_eq!(deserialized.error, "Configuration parse error");
1568 assert!(deserialized.details.is_some());
1569 assert_eq!(deserialized.details.unwrap().len(), 2);
1570 }
1571
1572 #[test]
1573 fn test_base_url_response_serialization() {
1574 use crate::runbeam_api::resources::BaseUrlResponse;
1575
1576 let response = BaseUrlResponse {
1577 base_url: "https://api.runbeam.io".to_string(),
1578 changes_path: Some("/api/changes".to_string()),
1579 full_url: Some("https://api.runbeam.io/api/changes".to_string()),
1580 };
1581
1582 let json = serde_json::to_string(&response).unwrap();
1583 assert!(json.contains("\"base_url\":\"https://api.runbeam.io\""));
1584
1585 let deserialized: BaseUrlResponse = serde_json::from_str(&json).unwrap();
1587 assert_eq!(deserialized.base_url, "https://api.runbeam.io");
1588 assert_eq!(deserialized.changes_path, Some("/api/changes".to_string()));
1589 assert_eq!(
1590 deserialized.full_url,
1591 Some("https://api.runbeam.io/api/changes".to_string())
1592 );
1593 }
1594
1595 #[test]
1596 fn test_store_config_request_serialization_with_id() {
1597 let request = StoreConfigRequest {
1598 config_type: "gateway".to_string(),
1599 id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
1600 config: "[proxy]\nid = \"test\"\n".to_string(),
1601 };
1602
1603 let json = serde_json::to_string(&request).unwrap();
1604 assert!(json.contains("\"type\":\"gateway\""));
1606 assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
1607 assert!(json.contains("\"config\":"));
1608 assert!(json.contains("[proxy]"));
1609
1610 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1612 assert_eq!(deserialized.config_type, "gateway");
1613 assert_eq!(
1614 deserialized.id,
1615 Some("01k8ek6h9aahhnrv3benret1nn".to_string())
1616 );
1617 }
1618
1619 #[test]
1620 fn test_store_config_request_serialization_without_id() {
1621 let request = StoreConfigRequest {
1622 config_type: "pipeline".to_string(),
1623 id: None,
1624 config: "[pipeline]\nname = \"test\"\n".to_string(),
1625 };
1626
1627 let json = serde_json::to_string(&request).unwrap();
1628 assert!(json.contains("\"type\":\"pipeline\""));
1629 assert!(json.contains("\"config\":"));
1630 assert!(!json.contains("\"id\""));
1632
1633 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1635 assert_eq!(deserialized.config_type, "pipeline");
1636 assert_eq!(deserialized.id, None);
1637 }
1638
1639 #[test]
1640 fn test_store_config_request_field_rename() {
1641 let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
1643 let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
1644 assert_eq!(request.config_type, "transform");
1645 assert_eq!(request.id, None);
1646
1647 let serialized = serde_json::to_string(&request).unwrap();
1649 assert!(serialized.contains("\"type\":"));
1650 assert!(!serialized.contains("\"config_type\":"));
1651 }
1652
1653 #[test]
1654 fn test_store_config_response_serialization() {
1655 use crate::runbeam_api::types::StoreConfigModel;
1656
1657 let response = StoreConfigResponse {
1658 success: true,
1659 message: "Configuration stored successfully".to_string(),
1660 data: StoreConfigModel {
1661 id: "01k9npa4tatmwddk66xxpcr2r0".to_string(),
1662 model_type: "gateway".to_string(),
1663 action: "updated".to_string(),
1664 },
1665 };
1666
1667 let json = serde_json::to_string(&response).unwrap();
1668 assert!(json.contains("\"success\":true"));
1669 assert!(json.contains("Configuration stored successfully"));
1670
1671 let deserialized: StoreConfigResponse = serde_json::from_str(&json).unwrap();
1673 assert_eq!(deserialized.success, true);
1674 assert_eq!(deserialized.message, "Configuration stored successfully");
1675 assert_eq!(deserialized.data.id, "01k9npa4tatmwddk66xxpcr2r0");
1676 }
1677
1678 #[test]
1679 fn test_acknowledge_changes_response_serialization() {
1680 use crate::runbeam_api::resources::AcknowledgeChangesResponse;
1681
1682 let response = AcknowledgeChangesResponse {
1684 acknowledged: vec![
1685 "change-1".to_string(),
1686 "change-2".to_string(),
1687 "change-3".to_string(),
1688 ],
1689 failed: vec![],
1690 };
1691
1692 let json = serde_json::to_string(&response).unwrap();
1693 assert!(json.contains("\"acknowledged\":"));
1694 assert!(json.contains("\"failed\":"));
1695 assert!(json.contains("change-1"));
1696
1697 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1699 assert_eq!(deserialized.acknowledged.len(), 3);
1700 assert_eq!(deserialized.failed.len(), 0);
1701
1702 let response_with_failures = AcknowledgeChangesResponse {
1704 acknowledged: vec!["change-1".to_string()],
1705 failed: vec!["change-2".to_string(), "change-3".to_string()],
1706 };
1707
1708 let json = serde_json::to_string(&response_with_failures).unwrap();
1709 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1710 assert_eq!(deserialized.acknowledged.len(), 1);
1711 assert_eq!(deserialized.failed.len(), 2);
1712 }
1713
1714 #[test]
1715 fn test_change_status_response_serialization() {
1716 use crate::runbeam_api::resources::{
1717 ChangeAppliedResponse, ChangeFailedResponse, ChangeStatusResponse,
1718 };
1719
1720 let response = ChangeStatusResponse {
1722 success: true,
1723 message: "Change marked as applied".to_string(),
1724 };
1725
1726 let json = serde_json::to_string(&response).unwrap();
1727 assert!(json.contains("\"success\":true"));
1728 assert!(json.contains("\"message\":\"Change marked as applied\""));
1729
1730 let deserialized: ChangeStatusResponse = serde_json::from_str(&json).unwrap();
1732 assert_eq!(deserialized.success, true);
1733 assert_eq!(deserialized.message, "Change marked as applied");
1734
1735 let applied_response: ChangeAppliedResponse = ChangeStatusResponse {
1737 success: true,
1738 message: "Change marked as applied".to_string(),
1739 };
1740
1741 let json = serde_json::to_string(&applied_response).unwrap();
1742 let deserialized: ChangeAppliedResponse = serde_json::from_str(&json).unwrap();
1743 assert_eq!(deserialized.success, true);
1744
1745 let failed_response: ChangeFailedResponse = ChangeStatusResponse {
1747 success: true,
1748 message: "Change marked as failed".to_string(),
1749 };
1750
1751 let json = serde_json::to_string(&failed_response).unwrap();
1752 let deserialized: ChangeFailedResponse = serde_json::from_str(&json).unwrap();
1753 assert_eq!(deserialized.success, true);
1754 assert_eq!(deserialized.message, "Change marked as failed");
1755 }
1756}