nap_core/provider/
portals_cloud.rs1use anyhow::{Context, Result};
8use tracing::{info, warn};
9
10use super::{Provider, ProviderStatus, ProviderType};
11
12pub struct PortalsCloudProvider {
14 api_url: String,
15 workspace_id: String,
16 account_id: Option<String>,
17 auth_token: Option<String>,
18}
19
20impl Default for PortalsCloudProvider {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl PortalsCloudProvider {
27 pub fn new() -> Self {
29 Self {
30 api_url: "https://api.portals.works".to_string(),
31 workspace_id: super::get_default_workspace_id(),
32 account_id: std::env::var("NAP_PORTALS_ACCOUNT_ID").ok(),
33 auth_token: std::env::var("NAP_PORTALS_AUTH_TOKEN").ok(),
34 }
35 }
36
37 pub fn with_api_url(mut self, url: &str) -> Self {
39 self.api_url = url.to_string();
40 self
41 }
42
43 pub fn with_workspace_id(mut self, workspace_id: &str) -> Self {
45 self.workspace_id = workspace_id.to_string();
46 self
47 }
48
49 pub fn with_account_id(mut self, account_id: &str) -> Self {
51 self.account_id = Some(account_id.to_string());
52 self
53 }
54
55 pub fn with_auth_token(mut self, token: &str) -> Self {
57 self.auth_token = Some(token.to_string());
58 self
59 }
60
61 fn is_authenticated(&self) -> bool {
63 self.account_id.is_some() && self.auth_token.is_some()
64 }
65
66 fn resolve_lore_url(&self) -> Result<String> {
68 if !self.is_authenticated() {
69 anyhow::bail!(
70 "Portals Cloud provider requires authentication (NAP_PORTALS_ACCOUNT_ID and NAP_PORTALS_AUTH_TOKEN)"
71 );
72 }
73
74 Ok(format!(
78 "lore://{}.portals.works",
79 self.account_id.as_ref().unwrap()
80 ))
81 }
82}
83
84#[async_trait::async_trait]
85impl Provider for PortalsCloudProvider {
86 fn provider_type(&self) -> ProviderType {
87 ProviderType::PortalsCloud
88 }
89
90 fn name(&self) -> &str {
91 "Portals Cloud"
92 }
93
94 async fn initialize(&self) -> Result<()> {
95 info!("Initializing Portals Cloud provider");
96
97 if !self.is_authenticated() {
98 warn!("Portals Cloud provider not authenticated");
99 }
100
101 info!("Portals Cloud provider initialized");
102 Ok(())
103 }
104
105 async fn ensure_ready(&self) -> Result<()> {
106 info!("Ensuring Portals Cloud provider is ready");
107
108 self.initialize().await?;
109
110 if !self.is_authenticated() {
111 anyhow::bail!("Portals Cloud provider requires authentication");
112 }
113
114 let health_url = format!("{}/health", self.api_url);
116 let response = reqwest::get(&health_url)
117 .await
118 .context("Failed to connect to Portals Cloud API")?;
119
120 if !response.status().is_success() {
121 anyhow::bail!(
122 "Portals Cloud API health check failed: {}",
123 response.status()
124 );
125 }
126
127 info!("Portals Cloud provider is ready");
128 Ok(())
129 }
130
131 fn lore_url_base(&self) -> Result<String> {
132 self.resolve_lore_url()
133 }
134
135 fn workspace_id(&self) -> &str {
136 &self.workspace_id
137 }
138
139 async fn health_check(&self) -> Result<bool> {
140 if !self.is_authenticated() {
141 return Ok(false);
142 }
143
144 let health_url = format!("{}/health", self.api_url);
145 match reqwest::get(&health_url).await {
146 Ok(response) => Ok(response.status().is_success()),
147 Err(_) => Ok(false),
148 }
149 }
150
151 async fn status(&self) -> Result<ProviderStatus> {
152 let authenticated = self.is_authenticated();
153 let healthy = self.health_check().await.unwrap_or(false);
154 let url_base = if authenticated {
155 self.lore_url_base()
156 .unwrap_or_else(|_| "unknown".to_string())
157 } else {
158 "not authenticated".to_string()
159 };
160
161 let message = if !authenticated {
162 "Not authenticated".to_string()
163 } else if !healthy {
164 "API unreachable".to_string()
165 } else {
166 "Connected".to_string()
167 };
168
169 Ok(ProviderStatus {
170 provider_type: self.provider_type(),
171 ready: authenticated && healthy,
172 healthy,
173 url_base,
174 workspace_id: self.workspace_id.clone(),
175 message,
176 })
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_portals_cloud_provider_creation() {
186 let provider = PortalsCloudProvider::new();
187 assert_eq!(provider.provider_type(), ProviderType::PortalsCloud);
188 assert_eq!(provider.name(), "Portals Cloud");
189 assert_eq!(provider.workspace_id(), "default");
190 }
191
192 #[test]
193 fn test_portals_cloud_provider_custom_config() {
194 let provider = PortalsCloudProvider::new()
195 .with_api_url("https://custom.api.com")
196 .with_workspace_id("custom-workspace")
197 .with_account_id("account-123")
198 .with_auth_token("token-456");
199
200 assert_eq!(provider.api_url, "https://custom.api.com");
201 assert_eq!(provider.workspace_id(), "custom-workspace");
202 assert_eq!(provider.account_id, Some("account-123".to_string()));
203 assert_eq!(provider.auth_token, Some("token-456".to_string()));
204 }
205
206 #[test]
207 fn test_authentication_check() {
208 let provider = PortalsCloudProvider::new();
209 assert!(!provider.is_authenticated());
210
211 let provider = PortalsCloudProvider::new()
212 .with_account_id("account-123")
213 .with_auth_token("token-456");
214 assert!(provider.is_authenticated());
215 }
216}