Skip to content

Commit 1d253a1

Browse files
authored
Improved Typesafety and Error Handling, Rustdoc (#2)
Improved Typesafety and Error Handling, Rustdoc
2 parents 82b1374 + afc0ec2 commit 1d253a1

File tree

4 files changed

+349
-208
lines changed

4 files changed

+349
-208
lines changed

Cargo.lock

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
[package]
2-
name = "rust-influxdb"
2+
name = "influxdb"
33
version = "0.1.0"
44
authors = ["Gero Gerke <[email protected]>"]
55
edition = "2018"
66

7-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8-
97
[dependencies]
108
reqwest = "0.9.17"
119
futures = "0.1.27"
1210
tokio = "0.1.20"
1311
itertools = "0.8"
12+
failure = "0.1.5"

src/lib.rs

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#![allow(dead_code)]
2+
3+
#[macro_use]
4+
extern crate failure;
5+
6+
use futures::Future;
7+
use itertools::Itertools;
8+
use reqwest::r#async::Client;
9+
10+
#[derive(Debug, Fail)]
11+
/// Errors that might happen in the crate
12+
pub enum InfluxDbError {
13+
#[fail(display = "query must contain at least one field")]
14+
/// Error happens when query has zero fields
15+
InvalidQueryError,
16+
}
17+
18+
#[derive(Debug)]
19+
#[doc(hidden)]
20+
pub struct ValidQuery(String);
21+
impl PartialEq<String> for ValidQuery {
22+
fn eq(&self, other: &String) -> bool {
23+
&self.0 == other
24+
}
25+
}
26+
impl PartialEq<&str> for ValidQuery {
27+
fn eq(&self, other: &&str) -> bool {
28+
&self.0 == other
29+
}
30+
}
31+
32+
/// Used to create read or [`InfluxDbWriteQuery`] queries to InfluxDB
33+
///
34+
/// # Examples
35+
///
36+
/// ```rust
37+
/// use influxdb::InfluxDbQuery;
38+
///
39+
/// let write_query = InfluxDbQuery::write_query("measurement")
40+
/// .add_field("field1", "5")
41+
/// .add_tag("tag1", "Gero")
42+
/// .build();
43+
///
44+
/// assert!(query.is_ok());
45+
///
46+
/// //todo: document read query once it's implemented.
47+
/// ```
48+
pub trait InfluxDbQuery {
49+
/// Builds valid InfluxSQL which can be run against the Database.
50+
/// In case no fields have been specified, it will return an error,
51+
/// as that is invalid InfluxSQL syntax.
52+
///
53+
/// # Examples
54+
///
55+
/// ```rust
56+
/// use influxdb::InfluxDbQuery;
57+
///
58+
/// let invalid_query = InfluxDbQuery::write_query("measurement").build();
59+
/// assert!(query.is_err());
60+
///
61+
/// let valid_query = InfluxDbQuery::write_query("measurement").add_field("myfield1", "11").build();
62+
/// assert!(query.is_ok());
63+
/// ```
64+
fn build<'a>(self) -> Result<ValidQuery, InfluxDbError>;
65+
}
66+
67+
impl InfluxDbQuery {
68+
/// Returns a [`InfluxDbWriteQuery`] builder.
69+
///
70+
/// # Examples
71+
///
72+
/// ```rust
73+
/// use influxdb::InfluxDbQuery;
74+
///
75+
/// InfluxDbQuery::write_query("measurement"); // Is of type [`InfluxDbWriteQuery`]
76+
/// ```
77+
pub fn write_query<S>(measurement: S) -> InfluxDbWriteQuery
78+
where
79+
S: Into<String>,
80+
{
81+
InfluxDbWriteQuery {
82+
measurement: measurement.into(),
83+
fields: Vec::new(),
84+
tags: Vec::new(),
85+
}
86+
}
87+
88+
// pub fn read() {}
89+
}
90+
91+
/// Write Query Builder returned by [InfluxDbQuery::write_query]()
92+
///
93+
/// Can only be instantiated by using [InfluxDbQuery::write_query]()
94+
pub struct InfluxDbWriteQuery {
95+
fields: Vec<(String, String)>,
96+
tags: Vec<(String, String)>,
97+
measurement: String,
98+
// precision: Precision
99+
}
100+
101+
impl InfluxDbWriteQuery {
102+
/// Adds a field to the [`InfluxDbWriteQuery`]
103+
///
104+
/// # Examples
105+
///
106+
/// ```rust
107+
/// use influxdb::InfluxDbQuery;
108+
///
109+
/// InfluxDbQuery::write_query("measurement").add_field("field1", "5").build();
110+
/// ```
111+
pub fn add_field<'a, S>(mut self, point: S, value: S) -> Self
112+
where
113+
S: Into<String>,
114+
{
115+
self.fields.push((point.into(), value.into()));
116+
self
117+
}
118+
119+
/// Adds a tag to the [`InfluxDbWriteQuery`]
120+
///
121+
/// Please note that a [`InfluxDbWriteQuery`] requires at least one field. Composing a query with
122+
/// only tags will result in a failure building the query.
123+
///
124+
/// # Examples
125+
///
126+
/// ```rust
127+
/// use influxdb::InfluxDbQuery;
128+
///
129+
/// InfluxDbQuery::write_query("measurement")
130+
/// .add_tag("field1", "5"); // calling `.build()` now would result in a `Err(InfluxDbError::InvalidQueryError)`
131+
/// ```
132+
pub fn add_tag<'a, S>(mut self, tag: S, value: S) -> Self
133+
where
134+
S: Into<String>,
135+
{
136+
self.tags.push((tag.into(), value.into()));
137+
self
138+
}
139+
}
140+
141+
// todo: fuse_with(other: ValidQuery), so multiple queries can be run at the same time
142+
impl InfluxDbQuery for InfluxDbWriteQuery {
143+
// todo: time (with precision)
144+
fn build<'a>(self) -> Result<ValidQuery, InfluxDbError> {
145+
if self.fields.is_empty() {
146+
return Err(InfluxDbError::InvalidQueryError);
147+
}
148+
149+
let tags = self
150+
.tags
151+
.into_iter()
152+
.map(|(tag, value)| format!("{tag}={value}", tag = tag, value = value))
153+
.join(",")
154+
+ " ";
155+
let fields = self
156+
.fields
157+
.into_iter()
158+
.map(|(field, value)| format!("{field}={value}", field = field, value = value))
159+
.join(",")
160+
+ " ";
161+
162+
Ok(ValidQuery(format!(
163+
"{measurement},{tags}{fields}time",
164+
measurement = self.measurement,
165+
tags = tags,
166+
fields = fields
167+
)))
168+
}
169+
}
170+
171+
/// Client which can read and write data from InfluxDB.
172+
///
173+
/// # Arguments
174+
///
175+
/// * `url`: The URL where InfluxDB is running (ex. `http://localhost:8086`).
176+
/// * `database`: The Database against which queries and writes will be run.
177+
///
178+
/// # Examples
179+
///
180+
/// ```rust
181+
/// use influxdb::InfluxDbClient;
182+
///
183+
/// let client = InfluxDbClient::new("http://localhost:8086", "test");
184+
///
185+
/// assert_eq!(client.get_database_name(), "test");
186+
/// ```
187+
pub struct InfluxDbClient {
188+
url: String,
189+
database: String,
190+
// auth: Option<InfluxDbAuthentication>
191+
}
192+
193+
impl InfluxDbClient {
194+
/// Instantiates a new [`InfluxDbClient`]
195+
///
196+
/// # Arguments
197+
///
198+
/// * `url`: The URL where InfluxDB is running (ex. `http://localhost:8086`).
199+
/// * `database`: The Database against which queries and writes will be run.
200+
///
201+
/// # Examples
202+
///
203+
/// ```rust
204+
/// use influxdb::InfluxDbClient;
205+
///
206+
/// let _client = InfluxDbClient::new("http://localhost:8086", "test");
207+
/// ```
208+
pub fn new<S>(url: S, database: S) -> Self
209+
where
210+
S: Into<String>,
211+
{
212+
InfluxDbClient {
213+
url: url.into(),
214+
database: database.into(),
215+
}
216+
}
217+
218+
pub fn get_database_name(self) -> String {
219+
self.database
220+
}
221+
222+
pub fn get_database_url(self) -> String {
223+
self.url
224+
}
225+
226+
pub fn ping(&self) -> impl Future<Item = (String, String), Error = ()> {
227+
Client::new()
228+
.get(format!("{}/ping", self.url).as_str())
229+
.send()
230+
.map(|res| {
231+
let build = res
232+
.headers()
233+
.get("X-Influxdb-Build")
234+
.unwrap()
235+
.to_str()
236+
.unwrap();
237+
let version = res
238+
.headers()
239+
.get("X-Influxdb-Version")
240+
.unwrap()
241+
.to_str()
242+
.unwrap();
243+
244+
(String::from(build), String::from(version))
245+
})
246+
.map_err(|err| println!("request error: {}", err))
247+
}
248+
}
249+
250+
pub fn main() {}
251+
252+
#[cfg(test)]
253+
mod tests {
254+
use super::{InfluxDbClient, InfluxDbQuery};
255+
use tokio::runtime::current_thread::Runtime;
256+
257+
fn get_runtime() -> Runtime {
258+
Runtime::new().expect("Unable to create a runtime")
259+
}
260+
261+
fn create_client() -> InfluxDbClient {
262+
InfluxDbClient::new("http://localhost:8086", "test")
263+
}
264+
265+
#[test]
266+
fn test_ping() {
267+
let client = create_client();
268+
let result = get_runtime().block_on(client.ping());
269+
assert!(result.is_ok(), "Should be no error");
270+
271+
let (build, version) = result.unwrap();
272+
assert!(!build.is_empty(), "Build should not be empty");
273+
assert!(!version.is_empty(), "Build should not be empty");
274+
275+
println!("build: {} version: {}", build, version);
276+
}
277+
278+
#[test]
279+
fn test_write_builder_empty_query() {
280+
let query = InfluxDbQuery::write_query("marina_3").build();
281+
282+
assert!(query.is_err(), "Query was not empty");
283+
}
284+
285+
#[test]
286+
fn test_write_builder_single_field() {
287+
let query = InfluxDbQuery::write_query("marina_3")
288+
.add_field("water_level", "2")
289+
.build();
290+
291+
assert!(query.is_ok(), "Query was empty");
292+
assert_eq!(query.unwrap(), "marina_3, water_level=2 time");
293+
}
294+
295+
#[test]
296+
fn test_write_builder_multiple_fields() {
297+
let query = InfluxDbQuery::write_query("marina_3")
298+
.add_field("water_level", "2")
299+
.add_field("boat_count", "31")
300+
.add_field("algae_content", "0.85")
301+
.build();
302+
303+
assert!(query.is_ok(), "Query was empty");
304+
assert_eq!(
305+
query.unwrap(),
306+
"marina_3, water_level=2,boat_count=31,algae_content=0.85 time"
307+
);
308+
}
309+
310+
// fixme: quoting / escaping of long strings
311+
#[test]
312+
fn test_write_builder_only_tags() {
313+
let query = InfluxDbQuery::write_query("marina_3")
314+
.add_tag("marina_manager", "Smith")
315+
.build();
316+
317+
assert!(query.is_err(), "Query missing one or more fields");
318+
}
319+
320+
#[test]
321+
fn test_write_builder_full_query() {
322+
let query = InfluxDbQuery::write_query("marina_3")
323+
.add_field("water_level", "2")
324+
.add_field("boat_count", "31")
325+
.add_field("algae_content", "0.85")
326+
.add_tag("marina_manager", "Smith")
327+
.add_tag("manager_to_the_marina_manager", "Jonson")
328+
.build();
329+
330+
assert!(query.is_ok(), "Query was empty");
331+
assert_eq!(
332+
query.unwrap(),
333+
"marina_3,marina_manager=Smith,manager_to_the_marina_manager=Jonson water_level=2,boat_count=31,algae_content=0.85 time"
334+
);
335+
}
336+
}

0 commit comments

Comments
 (0)