1extern crate helix_macros;
2
3pub use helix_macros::helix_node;
4
5use reqwest::{Client, StatusCode};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Clone)]
10pub struct HelixDB {
11 port: Option<u16>,
12 client: Client,
13 endpoint: String,
14 api_key: Option<String>,
15}
16
17#[derive(Debug, Error)]
18pub enum HelixError {
19 #[error("Error communicating with server: {0}")]
20 ReqwestError(#[from] reqwest::Error),
21 #[error("Got Error from server: {details}")]
22 RemoteError { details: String },
23}
24
25pub trait HelixDBClient {
27 type Err: std::error::Error;
28
29 fn new(endpoint: Option<&str>, port: Option<u16>, api_key: Option<&str>) -> Self;
30 fn query<T, R>(
31 &self,
32 endpoint: &str,
33 data: &T,
34 ) -> impl std::future::Future<Output = Result<R, Self::Err>> + Send
35 where
36 T: Serialize + Sync,
37 R: for<'de> Deserialize<'de>;
38}
39
40impl HelixDBClient for HelixDB {
41 type Err = HelixError;
42 fn new(endpoint: Option<&str>, port: Option<u16>, api_key: Option<&str>) -> Self {
43 Self {
44 port: port,
45 client: Client::new(),
46 endpoint: endpoint.unwrap_or("http://localhost").to_string(),
47 api_key: api_key.map(|key| key.to_string()),
48 }
49 }
50
51 async fn query<T, R>(&self, endpoint: &str, data: &T) -> Result<R, HelixError>
52 where
53 T: Serialize + Sync,
54 R: for<'de> Deserialize<'de>,
55 {
56 let port = match self.port {
57 Some(port) => format!(":{}", port),
58 None => "".to_string(),
59 };
60
61 let url = format!("{}{}/{}", self.endpoint, port, endpoint);
62
63 let mut request = self.client.post(&url).json(data);
64
65 if let Some(ref api_key) = self.api_key {
67 request = request.header("x-api-key", api_key);
68 }
69
70 let response = request.send().await?;
71
72 match response.status() {
73 StatusCode::OK => response.json().await.map_err(Into::into),
74 code => match response.text().await {
76 Ok(t) => Err(HelixError::RemoteError { details: t }),
77 Err(_) => match code.canonical_reason() {
78 Some(r) => Err(HelixError::RemoteError {
79 details: r.to_string(),
80 }),
81 None => Err(HelixError::RemoteError {
82 details: format!("unkown error with code: {code}"),
83 }),
84 },
85 },
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::helix_node;
93 use super::*;
94
95 #[helix_node]
96 struct User {
97 name: String,
98 age: i32,
99 }
100
101 #[tokio::test]
102 async fn test_basic_query() {
103 let client = HelixDB::new(None, None, None);
104
105 #[derive(Serialize)]
107 struct UserInput {
108 name: String,
109 age: i32,
110 }
111
112 #[derive(Deserialize)]
113 struct UserOutput {
114 id: String,
115 name: String,
116 age: i32,
117 }
118
119 let input = UserInput {
120 name: "John".to_string(),
121 age: 20,
122 };
123
124 #[derive(Deserialize)]
125 struct Result {
126 pub user: UserOutput,
127 }
128
129 let _result: Result = client.query("add_user", &input).await.unwrap();
131 }
132
133 #[tokio::test]
134 async fn test_query_with_api_key() {
135 let client = HelixDB::new(None, None, Some("test-api-key"));
136
137 #[derive(Serialize)]
139 struct UserInput {
140 name: String,
141 age: i32,
142 }
143
144 #[derive(Deserialize)]
145 struct UserOutput {
146 id: String,
147 name: String,
148 age: i32,
149 }
150
151 let input = UserInput {
152 name: "Jane".to_string(),
153 age: 25,
154 };
155
156 #[derive(Deserialize)]
157 struct Result {
158 pub user: UserOutput,
159 }
160
161 let _result: Result = client.query("add_user", &input).await.unwrap();
163 }
164}