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
1291#[cfg(test)]
1292mod tests {
1293 use super::*;
1294
1295 #[test]
1296 fn test_client_creation() {
1297 let client = RunbeamClient::new("http://example.com");
1298 assert_eq!(client.base_url(), "http://example.com");
1299 }
1300
1301 #[test]
1302 fn test_client_creation_with_string() {
1303 let base_url = String::from("http://example.com");
1304 let client = RunbeamClient::new(base_url);
1305 assert_eq!(client.base_url(), "http://example.com");
1306 }
1307
1308 #[test]
1309 fn test_authorize_request_serialization() {
1310 let request = AuthorizeRequest {
1311 token: "test_token".to_string(),
1312 gateway_code: "gw123".to_string(),
1313 machine_public_key: Some("pubkey123".to_string()),
1314 metadata: None,
1315 };
1316
1317 let json = serde_json::to_string(&request).unwrap();
1318 assert!(json.contains("\"token\":\"test_token\""));
1319 assert!(json.contains("\"gateway_code\":\"gw123\""));
1320 assert!(json.contains("\"machine_public_key\":\"pubkey123\""));
1321 }
1322
1323 #[test]
1324 fn test_authorize_request_serialization_without_optional_fields() {
1325 let request = AuthorizeRequest {
1326 token: "test_token".to_string(),
1327 gateway_code: "gw123".to_string(),
1328 machine_public_key: None,
1329 metadata: None,
1330 };
1331
1332 let json = serde_json::to_string(&request).unwrap();
1333 assert!(json.contains("\"token\":\"test_token\""));
1334 assert!(json.contains("\"gateway_code\":\"gw123\""));
1335 assert!(!json.contains("machine_public_key"));
1337 assert!(!json.contains("metadata"));
1338 }
1339
1340 #[test]
1341 fn test_change_serialization() {
1342 use crate::runbeam_api::resources::Change;
1343
1344 let change_metadata = Change {
1346 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1347 status: Some("pending".to_string()),
1348 resource_type: "gateway".to_string(),
1349 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1350 pipeline_id: None,
1351 toml_config: None,
1352 metadata: None,
1353 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1354 acknowledged_at: None,
1355 applied_at: None,
1356 failed_at: None,
1357 error_message: None,
1358 error_details: None,
1359 };
1360
1361 let json = serde_json::to_string(&change_metadata).unwrap();
1362 assert!(json.contains("\"id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1363 assert!(json.contains("\"gateway_id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1364 assert!(json.contains("\"type\":\"gateway\""));
1365
1366 let deserialized: Change = serde_json::from_str(&json).unwrap();
1368 assert_eq!(deserialized.id, "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX");
1369 assert_eq!(deserialized.status, Some("pending".to_string()));
1370 assert_eq!(deserialized.resource_type, "gateway");
1371
1372 let change_detail = Change {
1374 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1375 status: Some("applied".to_string()),
1376 resource_type: "gateway".to_string(),
1377 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1378 pipeline_id: None,
1379 toml_config: Some("[proxy]\nname = \"test\"".to_string()),
1380 metadata: Some(serde_json::json!({"gateway_name": "test-gateway"})),
1381 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1382 acknowledged_at: Some("2025-01-07T01:00:05+00:00".to_string()),
1383 applied_at: Some("2025-01-07T01:00:10+00:00".to_string()),
1384 failed_at: None,
1385 error_message: None,
1386 error_details: None,
1387 };
1388
1389 let json = serde_json::to_string(&change_detail).unwrap();
1390 assert!(json.contains("toml_config"));
1391 assert!(json.contains("acknowledged_at"));
1392 assert!(json.contains("applied_at"));
1393
1394 let deserialized: Change = serde_json::from_str(&json).unwrap();
1396 assert!(deserialized.toml_config.is_some());
1397 assert!(deserialized.acknowledged_at.is_some());
1398 assert!(deserialized.applied_at.is_some());
1399 }
1400
1401 #[test]
1402 fn test_acknowledge_changes_request_serialization() {
1403 use crate::runbeam_api::resources::AcknowledgeChangesRequest;
1404
1405 let request = AcknowledgeChangesRequest {
1406 change_ids: vec![
1407 "change-1".to_string(),
1408 "change-2".to_string(),
1409 "change-3".to_string(),
1410 ],
1411 };
1412
1413 let json = serde_json::to_string(&request).unwrap();
1414 assert!(json.contains("\"change_ids\""));
1415 assert!(json.contains("\"change-1\""));
1416 assert!(json.contains("\"change-2\""));
1417 assert!(json.contains("\"change-3\""));
1418
1419 let deserialized: AcknowledgeChangesRequest = serde_json::from_str(&json).unwrap();
1421 assert_eq!(deserialized.change_ids.len(), 3);
1422 assert_eq!(deserialized.change_ids[0], "change-1");
1423 }
1424
1425 #[test]
1426 fn test_change_failed_request_serialization() {
1427 use crate::runbeam_api::resources::ChangeFailedRequest;
1428
1429 let request_with_details = ChangeFailedRequest {
1431 error: "Configuration parse error".to_string(),
1432 details: Some(vec![
1433 "Invalid JSON at line 42".to_string(),
1434 "Missing required field 'name'".to_string(),
1435 ]),
1436 };
1437
1438 let json = serde_json::to_string(&request_with_details).unwrap();
1439 assert!(json.contains("\"error\":\"Configuration parse error\""));
1440 assert!(json.contains("\"details\""));
1441 assert!(json.contains("Invalid JSON at line 42"));
1442
1443 let request_without_details = ChangeFailedRequest {
1445 error: "Unknown error".to_string(),
1446 details: None,
1447 };
1448
1449 let json = serde_json::to_string(&request_without_details).unwrap();
1450 assert!(json.contains("\"error\":\"Unknown error\""));
1451 assert!(!json.contains("\"details\"")); let deserialized: ChangeFailedRequest =
1455 serde_json::from_str(&serde_json::to_string(&request_with_details).unwrap()).unwrap();
1456 assert_eq!(deserialized.error, "Configuration parse error");
1457 assert!(deserialized.details.is_some());
1458 assert_eq!(deserialized.details.unwrap().len(), 2);
1459 }
1460
1461 #[test]
1462 fn test_base_url_response_serialization() {
1463 use crate::runbeam_api::resources::BaseUrlResponse;
1464
1465 let response = BaseUrlResponse {
1466 base_url: "https://api.runbeam.io".to_string(),
1467 changes_path: Some("/api/changes".to_string()),
1468 full_url: Some("https://api.runbeam.io/api/changes".to_string()),
1469 };
1470
1471 let json = serde_json::to_string(&response).unwrap();
1472 assert!(json.contains("\"base_url\":\"https://api.runbeam.io\""));
1473
1474 let deserialized: BaseUrlResponse = serde_json::from_str(&json).unwrap();
1476 assert_eq!(deserialized.base_url, "https://api.runbeam.io");
1477 assert_eq!(deserialized.changes_path, Some("/api/changes".to_string()));
1478 assert_eq!(
1479 deserialized.full_url,
1480 Some("https://api.runbeam.io/api/changes".to_string())
1481 );
1482 }
1483
1484 #[test]
1485 fn test_store_config_request_serialization_with_id() {
1486 let request = StoreConfigRequest {
1487 config_type: "gateway".to_string(),
1488 id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
1489 config: "[proxy]\nid = \"test\"\n".to_string(),
1490 };
1491
1492 let json = serde_json::to_string(&request).unwrap();
1493 assert!(json.contains("\"type\":\"gateway\""));
1495 assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
1496 assert!(json.contains("\"config\":"));
1497 assert!(json.contains("[proxy]"));
1498
1499 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1501 assert_eq!(deserialized.config_type, "gateway");
1502 assert_eq!(
1503 deserialized.id,
1504 Some("01k8ek6h9aahhnrv3benret1nn".to_string())
1505 );
1506 }
1507
1508 #[test]
1509 fn test_store_config_request_serialization_without_id() {
1510 let request = StoreConfigRequest {
1511 config_type: "pipeline".to_string(),
1512 id: None,
1513 config: "[pipeline]\nname = \"test\"\n".to_string(),
1514 };
1515
1516 let json = serde_json::to_string(&request).unwrap();
1517 assert!(json.contains("\"type\":\"pipeline\""));
1518 assert!(json.contains("\"config\":"));
1519 assert!(!json.contains("\"id\""));
1521
1522 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1524 assert_eq!(deserialized.config_type, "pipeline");
1525 assert_eq!(deserialized.id, None);
1526 }
1527
1528 #[test]
1529 fn test_store_config_request_field_rename() {
1530 let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
1532 let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
1533 assert_eq!(request.config_type, "transform");
1534 assert_eq!(request.id, None);
1535
1536 let serialized = serde_json::to_string(&request).unwrap();
1538 assert!(serialized.contains("\"type\":"));
1539 assert!(!serialized.contains("\"config_type\":"));
1540 }
1541
1542 #[test]
1543 fn test_store_config_response_serialization() {
1544 use crate::runbeam_api::types::StoreConfigModel;
1545
1546 let response = StoreConfigResponse {
1547 success: true,
1548 message: "Configuration stored successfully".to_string(),
1549 data: StoreConfigModel {
1550 id: "01k9npa4tatmwddk66xxpcr2r0".to_string(),
1551 model_type: "gateway".to_string(),
1552 action: "updated".to_string(),
1553 },
1554 };
1555
1556 let json = serde_json::to_string(&response).unwrap();
1557 assert!(json.contains("\"success\":true"));
1558 assert!(json.contains("Configuration stored successfully"));
1559
1560 let deserialized: StoreConfigResponse = serde_json::from_str(&json).unwrap();
1562 assert_eq!(deserialized.success, true);
1563 assert_eq!(deserialized.message, "Configuration stored successfully");
1564 assert_eq!(deserialized.data.id, "01k9npa4tatmwddk66xxpcr2r0");
1565 }
1566
1567 #[test]
1568 fn test_acknowledge_changes_response_serialization() {
1569 use crate::runbeam_api::resources::AcknowledgeChangesResponse;
1570
1571 let response = AcknowledgeChangesResponse {
1573 acknowledged: vec![
1574 "change-1".to_string(),
1575 "change-2".to_string(),
1576 "change-3".to_string(),
1577 ],
1578 failed: vec![],
1579 };
1580
1581 let json = serde_json::to_string(&response).unwrap();
1582 assert!(json.contains("\"acknowledged\":"));
1583 assert!(json.contains("\"failed\":"));
1584 assert!(json.contains("change-1"));
1585
1586 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1588 assert_eq!(deserialized.acknowledged.len(), 3);
1589 assert_eq!(deserialized.failed.len(), 0);
1590
1591 let response_with_failures = AcknowledgeChangesResponse {
1593 acknowledged: vec!["change-1".to_string()],
1594 failed: vec!["change-2".to_string(), "change-3".to_string()],
1595 };
1596
1597 let json = serde_json::to_string(&response_with_failures).unwrap();
1598 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1599 assert_eq!(deserialized.acknowledged.len(), 1);
1600 assert_eq!(deserialized.failed.len(), 2);
1601 }
1602
1603 #[test]
1604 fn test_change_status_response_serialization() {
1605 use crate::runbeam_api::resources::{
1606 ChangeAppliedResponse, ChangeFailedResponse, ChangeStatusResponse,
1607 };
1608
1609 let response = ChangeStatusResponse {
1611 success: true,
1612 message: "Change marked as applied".to_string(),
1613 };
1614
1615 let json = serde_json::to_string(&response).unwrap();
1616 assert!(json.contains("\"success\":true"));
1617 assert!(json.contains("\"message\":\"Change marked as applied\""));
1618
1619 let deserialized: ChangeStatusResponse = serde_json::from_str(&json).unwrap();
1621 assert_eq!(deserialized.success, true);
1622 assert_eq!(deserialized.message, "Change marked as applied");
1623
1624 let applied_response: ChangeAppliedResponse = ChangeStatusResponse {
1626 success: true,
1627 message: "Change marked as applied".to_string(),
1628 };
1629
1630 let json = serde_json::to_string(&applied_response).unwrap();
1631 let deserialized: ChangeAppliedResponse = serde_json::from_str(&json).unwrap();
1632 assert_eq!(deserialized.success, true);
1633
1634 let failed_response: ChangeFailedResponse = ChangeStatusResponse {
1636 success: true,
1637 message: "Change marked as failed".to_string(),
1638 };
1639
1640 let json = serde_json::to_string(&failed_response).unwrap();
1641 let deserialized: ChangeFailedResponse = serde_json::from_str(&json).unwrap();
1642 assert_eq!(deserialized.success, true);
1643 assert_eq!(deserialized.message, "Change marked as failed");
1644 }
1645}