1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug)]
9pub enum RunbeamError {
10 JwtValidation(String),
12 Api(ApiError),
14 Storage(crate::storage::StorageError),
16 Config(String),
18 Validation(crate::validation::ValidationError),
20}
21
22impl fmt::Display for RunbeamError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 RunbeamError::JwtValidation(msg) => write!(f, "JWT validation failed: {}", msg),
26 RunbeamError::Api(err) => write!(f, "API error: {}", err),
27 RunbeamError::Storage(err) => write!(f, "Storage error: {}", err),
28 RunbeamError::Config(msg) => write!(f, "Configuration error: {}", msg),
29 RunbeamError::Validation(err) => write!(f, "Validation error: {}", err),
30 }
31 }
32}
33
34impl std::error::Error for RunbeamError {}
35
36impl From<ApiError> for RunbeamError {
37 fn from(err: ApiError) -> Self {
38 RunbeamError::Api(err)
39 }
40}
41
42impl From<crate::storage::StorageError> for RunbeamError {
43 fn from(err: crate::storage::StorageError) -> Self {
44 RunbeamError::Storage(err)
45 }
46}
47
48impl From<jsonwebtoken::errors::Error> for RunbeamError {
49 fn from(err: jsonwebtoken::errors::Error) -> Self {
50 RunbeamError::JwtValidation(err.to_string())
51 }
52}
53
54impl From<crate::validation::ValidationError> for RunbeamError {
55 fn from(err: crate::validation::ValidationError) -> Self {
56 RunbeamError::Validation(err)
57 }
58}
59
60#[derive(Debug)]
62pub enum ApiError {
63 Network(String),
65 Http { status: u16, message: String },
67 Parse(String),
69 Request(String),
71}
72
73impl fmt::Display for ApiError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 ApiError::Network(msg) => write!(f, "Network error: {}", msg),
77 ApiError::Http { status, message } => {
78 write!(f, "HTTP {} error: {}", status, message)
79 }
80 ApiError::Parse(msg) => write!(f, "Parse error: {}", msg),
81 ApiError::Request(msg) => write!(f, "Request error: {}", msg),
82 }
83 }
84}
85
86impl std::error::Error for ApiError {}
87
88impl From<reqwest::Error> for ApiError {
89 fn from(err: reqwest::Error) -> Self {
90 if err.is_timeout() {
91 ApiError::Network("Request timeout".to_string())
92 } else if err.is_connect() {
93 ApiError::Network(format!("Connection failed: {}", err))
94 } else if let Some(status) = err.status() {
95 ApiError::Http {
96 status: status.as_u16(),
97 message: err.to_string(),
98 }
99 } else {
100 ApiError::Network(err.to_string())
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct UserInfo {
108 pub id: String,
109 pub email: String,
110 pub name: String,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct UserToken {
120 pub token: String,
122 #[serde(default)]
124 pub expires_at: Option<i64>,
125 #[serde(default)]
127 pub user: Option<UserInfo>,
128}
129
130impl UserToken {
131 pub fn new(token: String, expires_at: Option<i64>, user: Option<UserInfo>) -> Self {
133 Self {
134 token,
135 expires_at,
136 user,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct TeamInfo {
144 pub id: String,
145 pub name: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct GatewayInfo {
151 pub id: String,
152 pub code: String,
153 pub name: String,
154 #[serde(default)]
155 pub authorized_by: Option<AuthorizedBy>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct AuthorizedBy {
161 pub id: String,
162 pub name: String,
163 pub email: String,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AuthorizeResponse {
169 pub machine_token: String,
170 pub expires_in: f64,
171 pub expires_at: String,
172 pub gateway: GatewayInfo,
173 #[serde(default)]
174 pub abilities: Vec<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct StoreConfigRequest {
183 #[serde(rename = "type")]
185 pub config_type: String,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub id: Option<String>,
189 pub config: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct StoreConfigResponse {
198 pub success: bool,
200 pub message: String,
202 pub data: StoreConfigModel,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct StoreConfigModel {
209 pub id: String,
211 #[serde(rename = "type")]
213 pub model_type: String,
214 pub action: String,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct MeshInfo {
221 pub id: String,
223 pub name: String,
225 #[serde(rename = "type")]
227 pub mesh_type: String,
228 pub provider: String,
230 #[serde(default = "default_auth_type")]
232 pub auth_type: String,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub jwt_secret: Option<String>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub jwt_private_key_path: Option<String>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub jwt_public_key_path: Option<String>,
242 #[serde(default)]
244 pub ingress: Vec<String>,
245 #[serde(default)]
247 pub egress: Vec<String>,
248 #[serde(default = "default_true")]
250 pub enabled: bool,
251 #[serde(default)]
253 pub description: Option<String>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct MeshIngressInfo {
259 pub id: String,
261 pub name: String,
263 #[serde(rename = "type")]
265 pub ingress_type: String,
266 pub pipeline: String,
268 #[serde(default = "default_mode")]
270 pub mode: String,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub endpoint: Option<String>,
274 #[serde(default)]
276 pub urls: Vec<String>,
277 #[serde(default = "default_true")]
279 pub enabled: bool,
280 #[serde(default)]
282 pub description: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct MeshEgressInfo {
288 pub id: String,
290 pub name: String,
292 #[serde(rename = "type")]
294 pub egress_type: String,
295 pub pipeline: String,
297 #[serde(default = "default_mode")]
299 pub mode: String,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub backend: Option<String>,
303 #[serde(default = "default_true")]
305 pub enabled: bool,
306 #[serde(default)]
308 pub description: Option<String>,
309}
310
311fn default_true() -> bool {
312 true
313}
314
315fn default_mode() -> String {
316 "default".to_string()
317}
318
319fn default_auth_type() -> String {
320 "jwt".to_string()
321}
322
323fn default_poll_interval() -> u32 {
324 30
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ResolveResourceResponse {
334 pub data: ResolvedResource,
336 pub meta: ResolutionMeta,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ResolutionMeta {
343 pub provider: String,
345 pub resolved_at: String,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct ResolvedResource {
352 #[serde(rename = "type")]
354 pub resource_type: String,
355 pub id: String,
357 pub name: String,
359 #[serde(default)]
361 pub team_id: Option<String>,
362 #[serde(default = "default_true")]
364 pub enabled: bool,
365 #[serde(default)]
367 pub gateway_id: Option<String>,
368 #[serde(default)]
370 pub pipeline_id: Option<String>,
371 #[serde(default)]
373 pub mesh_id: Option<String>,
374 #[serde(default)]
376 pub urls: Vec<String>,
377 #[serde(default)]
379 pub protocol: Option<String>,
380 #[serde(default)]
382 pub mode: Option<String>,
383 #[serde(default)]
385 pub backend_id: Option<String>,
386 #[serde(default)]
388 pub service_id: Option<String>,
389 #[serde(default)]
391 pub endpoint_id: Option<String>,
392 #[serde(default)]
394 pub description: Option<String>,
395 #[serde(default)]
397 pub provider: Option<String>,
398 #[serde(default)]
400 pub auth_type: Option<String>,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ProviderConfig {
413 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub api: Option<String>,
416 #[serde(default = "default_true")]
418 pub enabled: bool,
419 #[serde(default = "default_poll_interval")]
421 pub poll_interval_secs: u32,
422}
423
424impl Default for ProviderConfig {
425 fn default() -> Self {
426 Self {
427 api: None,
428 enabled: true,
429 poll_interval_secs: 30,
430 }
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(rename_all = "lowercase")]
437pub enum ResourceType {
438 Ingress,
439 Egress,
440 Pipeline,
441 Endpoint,
442 Backend,
443 Mesh,
444}
445
446impl fmt::Display for ResourceType {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 match self {
449 ResourceType::Ingress => write!(f, "ingress"),
450 ResourceType::Egress => write!(f, "egress"),
451 ResourceType::Pipeline => write!(f, "pipeline"),
452 ResourceType::Endpoint => write!(f, "endpoint"),
453 ResourceType::Backend => write!(f, "backend"),
454 ResourceType::Mesh => write!(f, "mesh"),
455 }
456 }
457}
458
459impl std::str::FromStr for ResourceType {
460 type Err = String;
461
462 fn from_str(s: &str) -> Result<Self, Self::Err> {
463 match s.to_lowercase().as_str() {
464 "ingress" => Ok(ResourceType::Ingress),
465 "egress" => Ok(ResourceType::Egress),
466 "pipeline" => Ok(ResourceType::Pipeline),
467 "endpoint" => Ok(ResourceType::Endpoint),
468 "backend" => Ok(ResourceType::Backend),
469 "mesh" => Ok(ResourceType::Mesh),
470 _ => Err(format!("Unknown resource type: {}", s)),
471 }
472 }
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
477pub enum LookupBy {
478 Id(String),
480 Name(String),
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
494pub struct ResourceReference {
495 pub provider: String,
497 pub team: Option<String>,
499 pub resource_type: Option<ResourceType>,
501 pub lookup: LookupBy,
503}
504
505impl ResourceReference {
506 pub fn parse(input: &str) -> Result<Self, String> {
521 let parts: Vec<&str> = input.split('.').collect();
522
523 match parts.len() {
524 1 => Ok(ResourceReference {
526 provider: "local".to_string(),
527 team: None,
528 resource_type: None,
529 lookup: LookupBy::Name(parts[0].to_string()),
530 }),
531
532 3 => {
534 let provider = parts[0];
535 match parts[1] {
536 "name" => Ok(ResourceReference {
537 provider: provider.to_string(),
538 team: None,
539 resource_type: None,
540 lookup: LookupBy::Name(parts[2].to_string()),
541 }),
542 "id" => Ok(ResourceReference {
543 provider: provider.to_string(),
544 team: None,
545 resource_type: None,
546 lookup: LookupBy::Id(parts[2].to_string()),
547 }),
548 _ => Err(format!(
549 "Invalid reference format: expected 'name' or 'id', got '{}'",
550 parts[1]
551 )),
552 }
553 }
554
555 4 => {
557 let provider = parts[0];
558 let team = parts[1];
559 match parts[2] {
560 "id" => Ok(ResourceReference {
561 provider: provider.to_string(),
562 team: Some(team.to_string()),
563 resource_type: None,
564 lookup: LookupBy::Id(parts[3].to_string()),
565 }),
566 _ => Err(format!(
567 "Invalid reference format: expected 'id' at position 2, got '{}'",
568 parts[2]
569 )),
570 }
571 }
572
573 5 => {
575 let provider = parts[0];
576 let team = parts[1];
577 let resource_type: ResourceType = parts[2].parse()?;
578 match parts[3] {
579 "name" => Ok(ResourceReference {
580 provider: provider.to_string(),
581 team: Some(team.to_string()),
582 resource_type: Some(resource_type),
583 lookup: LookupBy::Name(parts[4].to_string()),
584 }),
585 "id" => Ok(ResourceReference {
586 provider: provider.to_string(),
587 team: Some(team.to_string()),
588 resource_type: Some(resource_type),
589 lookup: LookupBy::Id(parts[4].to_string()),
590 }),
591 _ => Err(format!(
592 "Invalid reference format: expected 'name' or 'id', got '{}'",
593 parts[3]
594 )),
595 }
596 }
597
598 _ => Err(format!(
599 "Invalid reference format: unexpected number of parts ({})",
600 parts.len()
601 )),
602 }
603 }
604
605 pub fn is_local(&self) -> bool {
607 self.provider == "local"
608 }
609
610 pub fn to_reference_string(&self) -> String {
612 let lookup_str = match &self.lookup {
613 LookupBy::Id(id) => format!("id.{}", id),
614 LookupBy::Name(name) => format!("name.{}", name),
615 };
616
617 match (&self.team, &self.resource_type) {
618 (Some(team), Some(rt)) => format!("{}.{}.{}.{}", self.provider, team, rt, lookup_str),
619 (Some(team), None) => format!("{}.{}.{}", self.provider, team, lookup_str),
620 (None, _) => {
621 if self.provider == "local" {
623 if let LookupBy::Name(name) = &self.lookup {
624 return name.clone();
625 }
626 }
627 format!("{}.{}", self.provider, lookup_str)
628 }
629 }
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn test_store_config_request_with_id() {
639 let request = StoreConfigRequest {
640 config_type: "gateway".to_string(),
641 id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
642 config: "[proxy]\nid = \"test\"\n".to_string(),
643 };
644
645 let json = serde_json::to_string(&request).unwrap();
646 assert!(json.contains("\"type\":\"gateway\""));
647 assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
648 assert!(json.contains("\"config\":"));
649 assert!(json.contains("[proxy]"));
650
651 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
653 assert_eq!(deserialized.config_type, "gateway");
654 assert_eq!(
655 deserialized.id,
656 Some("01k8ek6h9aahhnrv3benret1nn".to_string())
657 );
658 }
659
660 #[test]
661 fn test_store_config_request_without_id() {
662 let request = StoreConfigRequest {
663 config_type: "pipeline".to_string(),
664 id: None,
665 config: "[pipeline]\nname = \"test\"\n".to_string(),
666 };
667
668 let json = serde_json::to_string(&request).unwrap();
669 assert!(json.contains("\"type\":\"pipeline\""));
670 assert!(json.contains("\"config\":"));
671 assert!(!json.contains("\"id\""));
673
674 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
676 assert_eq!(deserialized.config_type, "pipeline");
677 assert_eq!(deserialized.id, None);
678 }
679
680 #[test]
681 fn test_store_config_request_type_field_rename() {
682 let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
684 let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
685 assert_eq!(request.config_type, "transform");
686 assert_eq!(request.id, None);
687 }
688
689 #[test]
690 fn test_store_config_response() {
691 let json = r#"{
692 "success": true,
693 "message": "Gateway configuration updated successfully",
694 "data": {
695 "id": "01k9npa4tatmwddk66xxpcr2r0",
696 "type": "gateway",
697 "action": "updated"
698 }
699 }"#;
700
701 let response: StoreConfigResponse = serde_json::from_str(json).unwrap();
702 assert_eq!(response.success, true);
703 assert!(response.message.contains("updated successfully"));
704 assert_eq!(response.data.id, "01k9npa4tatmwddk66xxpcr2r0");
705 assert_eq!(response.data.model_type, "gateway");
706 assert_eq!(response.data.action, "updated");
707 }
708
709 #[test]
714 fn test_resource_reference_bare_name() {
715 let r = ResourceReference::parse("my_ingress").unwrap();
716 assert_eq!(r.provider, "local");
717 assert_eq!(r.team, None);
718 assert_eq!(r.resource_type, None);
719 assert_eq!(r.lookup, LookupBy::Name("my_ingress".to_string()));
720 assert!(r.is_local());
721 }
722
723 #[test]
724 fn test_resource_reference_local_name() {
725 let r = ResourceReference::parse("local.name.fhir_api").unwrap();
726 assert_eq!(r.provider, "local");
727 assert_eq!(r.team, None);
728 assert_eq!(r.resource_type, None);
729 assert_eq!(r.lookup, LookupBy::Name("fhir_api".to_string()));
730 assert!(r.is_local());
731 }
732
733 #[test]
734 fn test_resource_reference_provider_id() {
735 let r = ResourceReference::parse("runbeam.id.01JGXYZ123ABC").unwrap();
736 assert_eq!(r.provider, "runbeam");
737 assert_eq!(r.team, None);
738 assert_eq!(r.resource_type, None);
739 assert_eq!(r.lookup, LookupBy::Id("01JGXYZ123ABC".to_string()));
740 assert!(!r.is_local());
741 }
742
743 #[test]
744 fn test_resource_reference_team_id() {
745 let r = ResourceReference::parse("runbeam.acme.id.01JGXYZ123ABC").unwrap();
746 assert_eq!(r.provider, "runbeam");
747 assert_eq!(r.team, Some("acme".to_string()));
748 assert_eq!(r.resource_type, None);
749 assert_eq!(r.lookup, LookupBy::Id("01JGXYZ123ABC".to_string()));
750 }
751
752 #[test]
753 fn test_resource_reference_full_path_name() {
754 let r = ResourceReference::parse("runbeam.acme_health.ingress.name.patient_api").unwrap();
755 assert_eq!(r.provider, "runbeam");
756 assert_eq!(r.team, Some("acme_health".to_string()));
757 assert_eq!(r.resource_type, Some(ResourceType::Ingress));
758 assert_eq!(r.lookup, LookupBy::Name("patient_api".to_string()));
759 }
760
761 #[test]
762 fn test_resource_reference_full_path_id() {
763 let r = ResourceReference::parse("runbeam.partner_lab.egress.id.01JGXYZ").unwrap();
764 assert_eq!(r.provider, "runbeam");
765 assert_eq!(r.team, Some("partner_lab".to_string()));
766 assert_eq!(r.resource_type, Some(ResourceType::Egress));
767 assert_eq!(r.lookup, LookupBy::Id("01JGXYZ".to_string()));
768 }
769
770 #[test]
771 fn test_resource_reference_all_types() {
772 assert!(ResourceReference::parse("runbeam.t.ingress.name.x").unwrap().resource_type == Some(ResourceType::Ingress));
773 assert!(ResourceReference::parse("runbeam.t.egress.name.x").unwrap().resource_type == Some(ResourceType::Egress));
774 assert!(ResourceReference::parse("runbeam.t.pipeline.name.x").unwrap().resource_type == Some(ResourceType::Pipeline));
775 assert!(ResourceReference::parse("runbeam.t.endpoint.name.x").unwrap().resource_type == Some(ResourceType::Endpoint));
776 assert!(ResourceReference::parse("runbeam.t.backend.name.x").unwrap().resource_type == Some(ResourceType::Backend));
777 assert!(ResourceReference::parse("runbeam.t.mesh.name.x").unwrap().resource_type == Some(ResourceType::Mesh));
778 }
779
780 #[test]
781 fn test_resource_reference_invalid_format() {
782 assert!(ResourceReference::parse("runbeam.team.invalid.name.x").is_err());
783 assert!(ResourceReference::parse("a.b").is_err());
784 assert!(ResourceReference::parse("a.b.c.d.e.f").is_err());
785 }
786
787 #[test]
788 fn test_resource_reference_to_string() {
789 let r = ResourceReference::parse("my_ingress").unwrap();
791 assert_eq!(r.to_reference_string(), "my_ingress");
792
793 let r = ResourceReference::parse("runbeam.acme.ingress.name.patient_api").unwrap();
795 assert_eq!(r.to_reference_string(), "runbeam.acme.ingress.name.patient_api");
796
797 let r = ResourceReference::parse("runbeam.id.01JGXYZ").unwrap();
799 assert_eq!(r.to_reference_string(), "runbeam.id.01JGXYZ");
800 }
801
802 #[test]
803 fn test_provider_config_default() {
804 let config = ProviderConfig::default();
805 assert_eq!(config.api, None);
806 assert_eq!(config.enabled, true);
807 assert_eq!(config.poll_interval_secs, 30);
808 }
809
810 #[test]
811 fn test_provider_config_serde() {
812 let json = r#"{"api":"https://app.runbeam.io","enabled":true,"poll_interval_secs":60}"#;
813 let config: ProviderConfig = serde_json::from_str(json).unwrap();
814 assert_eq!(config.api, Some("https://app.runbeam.io".to_string()));
815 assert_eq!(config.enabled, true);
816 assert_eq!(config.poll_interval_secs, 60);
817 }
818}