1use async_trait::async_trait;
6use reqwest::Client;
7
8use std::collections::HashMap;
9
10use crate::client::GraphmindClient;
11use crate::error::{GraphmindError, GraphmindResult};
12use crate::models::{QueryResult, ServerStatus};
13
14pub struct RemoteClient {
18 http_base_url: String,
19 http_client: Client,
20}
21
22impl RemoteClient {
23 pub fn new(http_base_url: &str) -> Self {
31 Self {
32 http_base_url: http_base_url.trim_end_matches('/').to_string(),
33 http_client: Client::new(),
34 }
35 }
36
37 async fn post_query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
39 self.post_query_with_params(graph, cypher, None).await
40 }
41
42 async fn post_query_with_params(
44 &self,
45 graph: &str,
46 cypher: &str,
47 params: Option<HashMap<String, serde_json::Value>>,
48 ) -> GraphmindResult<QueryResult> {
49 let url = format!("{}/api/query", self.http_base_url);
50 let mut body = serde_json::json!({ "query": cypher, "graph": graph });
51 if let Some(p) = params {
52 body["params"] = serde_json::to_value(p)
53 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
54 }
55
56 let response = self.http_client.post(&url).json(&body).send().await?;
57
58 if response.status().is_success() {
59 let result: QueryResult = response.json().await?;
60 Ok(result)
61 } else {
62 let error_body: serde_json::Value = response
63 .json()
64 .await
65 .unwrap_or_else(|_| serde_json::json!({"error": "Unknown error"}));
66 let msg = error_body
67 .get("error")
68 .and_then(|v| v.as_str())
69 .unwrap_or("Unknown error")
70 .to_string();
71 Err(GraphmindError::QueryError(msg))
72 }
73 }
74}
75
76#[async_trait]
77impl GraphmindClient for RemoteClient {
78 async fn query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
79 self.post_query(graph, cypher).await
80 }
81
82 async fn query_with_params(
83 &self,
84 graph: &str,
85 cypher: &str,
86 params: HashMap<String, serde_json::Value>,
87 ) -> GraphmindResult<QueryResult> {
88 self.post_query_with_params(graph, cypher, Some(params))
89 .await
90 }
91
92 async fn query_readonly(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
93 self.post_query(graph, cypher).await
94 }
95
96 async fn delete_graph(&self, graph: &str) -> GraphmindResult<()> {
97 let _ = self.post_query(graph, "MATCH (n) DETACH DELETE n").await;
99 let url = format!("{}/api/graphs/{}", self.http_base_url, graph);
101 let _ = self.http_client.delete(&url).send().await;
102 Ok(())
103 }
104
105 async fn list_graphs(&self) -> GraphmindResult<Vec<String>> {
106 Ok(vec!["default".to_string()])
108 }
109
110 async fn status(&self, graph: &str) -> GraphmindResult<ServerStatus> {
111 let url = format!("{}/api/status?graph={}", self.http_base_url, graph);
112 let response = self.http_client.get(&url).send().await?;
113
114 if response.status().is_success() {
115 let status: ServerStatus = response.json().await?;
116 Ok(status)
117 } else {
118 Err(GraphmindError::ConnectionError(format!(
119 "Status endpoint returned {}",
120 response.status()
121 )))
122 }
123 }
124
125 async fn ping(&self) -> GraphmindResult<String> {
126 let status = self.status("default").await?;
127 if status.status == "healthy" {
128 Ok("PONG".to_string())
129 } else {
130 Err(GraphmindError::ConnectionError(format!(
131 "Server unhealthy: {}",
132 status.status
133 )))
134 }
135 }
136
137 async fn schema(&self, graph: &str) -> GraphmindResult<String> {
138 let url = format!("{}/api/schema?graph={}", self.http_base_url, graph);
139 let response = self.http_client.get(&url).send().await?;
140
141 if response.status().is_success() {
142 let body: serde_json::Value = response.json().await?;
143 Ok(serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string()))
144 } else {
145 Err(GraphmindError::ConnectionError(format!(
146 "Schema endpoint returned {}",
147 response.status()
148 )))
149 }
150 }
151
152 async fn explain(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
153 let prefixed = if cypher.trim().to_uppercase().starts_with("EXPLAIN") {
154 cypher.to_string()
155 } else {
156 format!("EXPLAIN {}", cypher)
157 };
158 self.post_query(graph, &prefixed).await
159 }
160
161 async fn profile(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
162 let prefixed = if cypher.trim().to_uppercase().starts_with("PROFILE") {
163 cypher.to_string()
164 } else {
165 format!("PROFILE {}", cypher)
166 };
167 self.post_query(graph, &prefixed).await
168 }
169}