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    fn http_health_url(&self) -> Result<String> {
46        // Convert lore://host:port to http://host:port
47        let url = if self.url_base.starts_with("lore://") {
48            self.url_base.replace("lore://", "http://")
49        } else if self.url_base.starts_with("lores://") {
50            self.url_base.replace("lores://", "https://")
51        } else {
52            anyhow::bail!("Invalid Lore URL format: {}", self.url_base);
53        };
54
55        Ok(format!("{}/health_check", url))
56    }
57}
58
59#[async_trait::async_trait]
60impl Provider for RemoteProvider {
61    fn provider_type(&self) -> ProviderType {
62        ProviderType::Remote
63    }
64
65    fn name(&self) -> &str {
66        "Remote Lore Server"
67    }
68
69    async fn initialize(&self) -> Result<()> {
70        info!("Initializing Remote provider for {}", self.url_base);
71        info!("Remote provider initialized");
72        Ok(())
73    }
74
75    async fn ensure_ready(&self) -> Result<()> {
76        info!("Ensuring Remote provider is ready");
77
78        self.initialize().await?;
79
80        // Check connectivity to remote server
81        let health_url = self.http_health_url()?;
82        let response = reqwest::get(&health_url)
83            .await
84            .context("Failed to connect to remote Lore server")?;
85
86        if !response.status().is_success() {
87            anyhow::bail!(
88                "Remote Lore server health check failed: {}",
89                response.status()
90            );
91        }
92
93        info!("Remote provider is ready");
94        Ok(())
95    }
96
97    fn lore_url_base(&self) -> Result<String> {
98        Ok(self.url_base.clone())
99    }
100
101    fn workspace_id(&self) -> &str {
102        &self.workspace_id
103    }
104
105    async fn health_check(&self) -> Result<bool> {
106        let health_url = self.http_health_url()?;
107        match reqwest::get(&health_url).await {
108            Ok(response) => Ok(response.status().is_success()),
109            Err(_) => Ok(false),
110        }
111    }
112
113    async fn status(&self) -> Result<ProviderStatus> {
114        let healthy = self.health_check().await.unwrap_or(false);
115
116        let message = if healthy {
117            "Connected".to_string()
118        } else {
119            "Server unreachable".to_string()
120        };
121
122        Ok(ProviderStatus {
123            provider_type: self.provider_type(),
124            ready: healthy,
125            healthy,
126            url_base: self.url_base.clone(),
127            workspace_id: self.workspace_id.clone(),
128            message,
129        })
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_remote_provider_creation() {
139        let provider = RemoteProvider::new("lore://localhost:41337", "default");
140        assert_eq!(provider.provider_type(), ProviderType::Remote);
141        assert_eq!(provider.name(), "Remote Lore Server");
142        assert_eq!(provider.workspace_id(), "default");
143        assert_eq!(provider.url_base, "lore://localhost:41337");
144    }
145
146    #[test]
147    fn test_remote_provider_custom_auth() {
148        let provider = RemoteProvider::new("lore://localhost:41337", "default")
149            .with_auth_token("custom-token");
150        assert_eq!(provider.auth_token, Some("custom-token".to_string()));
151    }
152
153    #[test]
154    fn test_http_health_url() {
155        let provider = RemoteProvider::new("lore://localhost:41337", "default");
156        assert_eq!(
157            provider.http_health_url().unwrap(),
158            "http://localhost:41337/health_check"
159        );
160
161        let provider = RemoteProvider::new("lores://example.com:41337", "default");
162        assert_eq!(
163            provider.http_health_url().unwrap(),
164            "https://example.com:41337/health_check"
165        );
166    }
167}