Skip to main content

nap_core/provider/
portals_cloud.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Portals Cloud provider for Lore server
4//!
5//! Manages repository operations on the Portals Cloud hosted Lore service.
6
7use anyhow::{Context, Result};
8use tracing::{info, warn};
9
10use super::{Provider, ProviderStatus, ProviderType};
11
12/// Portals Cloud provider for hosted Lore service
13pub 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    /// Create a new Portals Cloud provider
28    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    /// Set custom API URL
38    pub fn with_api_url(mut self, url: &str) -> Self {
39        self.api_url = url.to_string();
40        self
41    }
42
43    /// Set custom workspace ID
44    pub fn with_workspace_id(mut self, workspace_id: &str) -> Self {
45        self.workspace_id = workspace_id.to_string();
46        self
47    }
48
49    /// Set account ID
50    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    /// Set auth token
56    pub fn with_auth_token(mut self, token: &str) -> Self {
57        self.auth_token = Some(token.to_string());
58        self
59    }
60
61    /// Check if authenticated
62    fn is_authenticated(&self) -> bool {
63        self.account_id.is_some() && self.auth_token.is_some()
64    }
65
66    /// Get Lore server URL from Portals Cloud
67    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        // In a real implementation, this would query the Portals Cloud API
75        // to get the Lore server URL for the account/workspace
76        // For now, return a placeholder
77        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        // Check connectivity to Portals Cloud API
115        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}