Skip to main content

nap_core/provider/
remote.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Remote provider for Lore server
4//!
5//! Manages repository operations on a remote Lore instance.
6
7use anyhow::{Context, Result};
8use tracing::info;
9
10use super::{Provider, ProviderStatus, ProviderType};
11
12/// Remote provider for custom Lore server
13pub struct RemoteProvider {
14    url_base: String,
15    workspace_id: String,
16    auth_token: Option<String>,
17}
18
19impl RemoteProvider {
20    /// Create a new remote provider
21    pub fn new(url_base: &str, workspace_id: &str) -> Self {
22        Self {
23            url_base: url_base.to_string(),
24            workspace_id: workspace_id.to_string(),
25            auth_token: std::env::var("NAP_REMOTE_AUTH_TOKEN").ok(),
26        }
27    }
28
29    /// Create a new remote provider with default workspace ID
30    pub fn new_with_default_workspace(url_base: &str) -> Self {
31        Self {
32            url_base: url_base.to_string(),
33            workspace_id: super::get_default_workspace_id(),
34            auth_token: std::env::var("NAP_REMOTE_AUTH_TOKEN").ok(),
35        }
36    }
37
38    /// Set custom auth token
39    pub fn with_auth_token(mut self, token: &str) -> Self {
40        self.auth_token = Some(token.to_string());
41        self
42    }
43
44    /// Parse URL to extract HTTP health check endpoint
45    ///
46    /// Lore server uses port 41337 for gRPC/QUIC (lore:// URLs) and port 41339 for HTTP.
47    /// This function converts lore://host:41337 to http://host:41339/health_check
48    fn http_health_url(&self) -> Result<String> {
49        // Parse the lore:// URL to extract host and optionally port
50        let (scheme, rest) = if self.url_base.starts_with("lore://") {
51            ("http", &self.url_base[7..]) // "lore://" is 7 characters
52        } else if self.url_base.starts_with("lores://") {
53            ("https", &self.url_base[8..]) // "lores://" is 8 characters
54        } else {
55            anyhow::bail!("Invalid Lore URL format: {}", self.url_base);
56        };
57
58        // Split host and port if present
59        let host = if rest.contains(':') {
60            // Extract host part, ignore the lore port (typically 41337)
61            rest.split(':').next().unwrap()
62        } else {
63            rest
64        };
65
66        // Lore server uses port 41339 for HTTP health checks
67        let http_port = 41339;
68
69        Ok(format!("{}://{}:{}/health_check", scheme, host, http_port))
70    }
71}
72
73#[async_trait::async_trait]
74impl Provider for RemoteProvider {
75    fn provider_type(&self) -> ProviderType {
76        ProviderType::Remote
77    }
78
79    fn name(&self) -> &str {
80        "Remote Lore Server"
81    }
82
83    async fn initialize(&self) -> Result<()> {
84        info!("Initializing Remote provider for {}", self.url_base);
85        info!("Remote provider initialized");
86        Ok(())
87    }
88
89    async fn ensure_ready(&self) -> Result<()> {
90        info!("Ensuring Remote provider is ready");
91
92        self.initialize().await?;
93
94        // Check connectivity to remote server
95        let health_url = self.http_health_url()?;
96        let response = reqwest::get(&health_url)
97            .await
98            .context("Failed to connect to remote Lore server")?;
99
100        if !response.status().is_success() {
101            anyhow::bail!(
102                "Remote Lore server health check failed: {}",
103                response.status()
104            );
105        }
106
107        info!("Remote provider is ready");
108        Ok(())
109    }
110
111    fn lore_url_base(&self) -> Result<String> {
112        Ok(self.url_base.clone())
113    }
114
115    fn workspace_id(&self) -> &str {
116        &self.workspace_id
117    }
118
119    async fn health_check(&self) -> Result<bool> {
120        let health_url = self.http_health_url()?;
121        match reqwest::get(&health_url).await {
122            Ok(response) => Ok(response.status().is_success()),
123            Err(_) => Ok(false),
124        }
125    }
126
127    async fn status(&self) -> Result<ProviderStatus> {
128        let healthy = self.health_check().await.unwrap_or(false);
129
130        let message = if healthy {
131            "Connected".to_string()
132        } else {
133            "Server unreachable".to_string()
134        };
135
136        Ok(ProviderStatus {
137            provider_type: self.provider_type(),
138            ready: healthy,
139            healthy,
140            url_base: self.url_base.clone(),
141            workspace_id: self.workspace_id.clone(),
142            message,
143        })
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_remote_provider_creation() {
153        let provider = RemoteProvider::new("lore://localhost:41337", "default");
154        assert_eq!(provider.provider_type(), ProviderType::Remote);
155        assert_eq!(provider.name(), "Remote Lore Server");
156        assert_eq!(provider.workspace_id(), "default");
157        assert_eq!(provider.url_base, "lore://localhost:41337");
158    }
159
160    #[test]
161    fn test_remote_provider_custom_auth() {
162        let provider = RemoteProvider::new("lore://localhost:41337", "default")
163            .with_auth_token("custom-token");
164        assert_eq!(provider.auth_token, Some("custom-token".to_string()));
165    }
166
167    #[test]
168    fn test_http_health_url() {
169        let provider = RemoteProvider::new("lore://localhost:41337", "default");
170        assert_eq!(
171            provider.http_health_url().unwrap(),
172            "http://localhost:41339/health_check"
173        );
174
175        let provider = RemoteProvider::new("lores://example.com:41337", "default");
176        assert_eq!(
177            provider.http_health_url().unwrap(),
178            "https://example.com:41339/health_check"
179        );
180
181        // Test without port in URL
182        let provider = RemoteProvider::new("lore://192.168.0.27", "default");
183        assert_eq!(
184            provider.http_health_url().unwrap(),
185            "http://192.168.0.27:41339/health_check"
186        );
187    }
188}