diff --git a/.travis.yml b/.travis.yml index 171cfbb..525b308 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,8 +51,13 @@ matrix: script: - bash auxiliary/check_readme_consistency.sh -script: | - export RUST_BACKTRACE=1 && - cargo build && - cargo test --all-features && - cargo doc --no-deps +script: + - export RUST_BACKTRACE=1 + - cd influxdb + - cargo build + - cargo test --all --all-features + - cargo doc --no-deps + - cd ../influxdb_derive + - cargo build + - cargo test --all --all-features + - cargo doc --no-deps diff --git a/Cargo.toml b/Cargo.toml index 3608ebf..c2a976b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,8 @@ -[package] -name = "influxdb" -version = "0.0.6" -authors = ["Gero Gerke <11deutron11@gmail.com>"] -edition = "2018" -description = "InfluxDB Driver for Rust" -keywords = ["influxdb", "database", "influx"] -license = "MIT" -readme = "README.md" -include = ["src/**/*", "tests/**/*", "Cargo.toml", "LICENSE"] -repository = "https://github.com/Empty2k12/influxdb-rust" +# -*- eval: (cargo-minor-mode 1) -*- -[badges] -travis-ci = { repository = "Empty2k12/influxdb-rust", branch = "master" } +[workspace] +members = ["influxdb", "influxdb_derive"] -[dependencies] -chrono = { version = "0.4.10", optional = true } -failure = "0.1.6" -futures = "0.3.4" -reqwest = { version = "0.10.1", features = ["json"] } -serde = { version = "1.0.104", features = ["derive"], optional = true } -serde_json = { version = "1.0.46", optional = true } -regex = "1.3.4" -lazy_static = "1.4.0" - -# This is a temporary work around to fix a Failure-derive compilation error -# Should be removed when https://github.com/Empty2k12/influxdb-rust/issues/48 is being done -quote = "=1.0.2" - -[features] -use-serde = ["serde", "serde_json"] -chrono_timestamps = ["chrono"] -default = ["use-serde"] - -[dev-dependencies] -tokio = { version = "0.2.11", features = ["macros"] } +[patch.crates-io] +influxdb = { path = "./influxdb" } +influxdb_derive = { path = "./influxdb_derive" } diff --git a/README.md b/README.md index bbd5412..74a4416 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,12 @@ Pull requests are always welcome. See [Contributing](https://github.com/Empty2k1 - Optional Serde Support for Deserialization - Running multiple queries in one request (e.g. `SELECT * FROM weather_berlin; SELECT * FROM weather_london`) - Authenticated and Unauthenticated Connections -- Optional conversion between `Timestamp` and `Chrono::DateTime` via `chrono_timestamps` compilation feature - `async`/`await` support +- `#[derive(InfluxDbWriteable)]` Derive Macro for Writing / Reading into Structs ### Planned Features - Read Query Builder instead of supplying raw queries -- `#[derive(InfluxDbReadable)]` and `#[derive(InfluxDbWriteable)]` proc macros ## Quickstart @@ -59,6 +58,7 @@ For an example with using Serde deserialization, please refer to [serde_integrat ```rust use influxdb::{Client, Query, Timestamp}; +use influxdb::InfluxDbWriteable; // Create a Client with URL `http://localhost:8086` // and database name `test` @@ -67,7 +67,7 @@ let client = Client::new("http://localhost:8086", "test"); // Let's write something to InfluxDB. First we're creating a // WriteQuery to write some data. // This creates a query which writes a new measurement into a series called `weather` -let write_query = Query::write_query(Timestamp::Now, "weather") +let write_query = Timestamp::Now.into_query("weather") .add_field("temperature", 82); // Submit the query to InfluxDB. diff --git a/auxiliary/check_readme_consistency.sh b/auxiliary/check_readme_consistency.sh index 524525c..f5c120a 100755 --- a/auxiliary/check_readme_consistency.sh +++ b/auxiliary/check_readme_consistency.sh @@ -1,6 +1,6 @@ -#!/usr/bin/bash +#!/bin/bash -cargo readme > README.md.expected +cargo readme -r influxdb -t ../README.tpl > README.md.expected diff README.md README.md.expected @@ -9,6 +9,6 @@ then echo 'README.md is up to date!' exit 0 else - echo 'README.md out of date. Run "cargo readme > README.md" and commit again.' + echo 'README.md out of date. Run "cargo readme -r influxdb -t ../README.tpl > README.md" and commit again.' exit 1 -fi \ No newline at end of file +fi diff --git a/influxdb/Cargo.toml b/influxdb/Cargo.toml new file mode 100644 index 0000000..5e645a6 --- /dev/null +++ b/influxdb/Cargo.toml @@ -0,0 +1,35 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "influxdb" +version = "0.0.6" +authors = ["Gero Gerke <11deutron11@gmail.com>"] +edition = "2018" +description = "InfluxDB Driver for Rust" +keywords = ["influxdb", "database", "influx"] +license = "MIT" +readme = "README.md" +include = ["src/**/*", "tests/**/*", "Cargo.toml", "LICENSE"] +repository = "https://github.com/Empty2k12/influxdb-rust" + +[badges] +travis-ci = { repository = "Empty2k12/influxdb-rust", branch = "master" } + +[dependencies] +chrono = { version = "0.4.10", features = ["serde"] } +failure = "0.1.6" +futures = "0.3.4" +influxdb_derive = { version = "0.0.1", optional = true } +reqwest = { version = "0.10.1", features = ["json"] } +serde = { version = "1.0.104", features = ["derive"], optional = true } +serde_json = { version = "1.0.46", optional = true } +regex = "1.3.4" +lazy_static = "1.4.0" + +[features] +use-serde = ["serde", "serde_json"] +default = ["use-serde"] +derive = ["influxdb_derive"] + +[dev-dependencies] +tokio = { version = "0.2.11", features = ["macros"] } diff --git a/influxdb/LICENSE b/influxdb/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/influxdb/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/influxdb/README.md b/influxdb/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/influxdb/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/src/client/mod.rs b/influxdb/src/client/mod.rs similarity index 98% rename from src/client/mod.rs rename to influxdb/src/client/mod.rs index 2911f85..d1c224b 100644 --- a/src/client/mod.rs +++ b/influxdb/src/client/mod.rs @@ -162,11 +162,13 @@ impl Client { /// /// ```rust,no_run /// use influxdb::{Client, Query, Timestamp}; + /// use influxdb::InfluxDbWriteable; /// /// # #[tokio::main] /// # async fn main() -> Result<(), failure::Error> { /// let client = Client::new("http://localhost:8086", "test"); - /// let query = Query::write_query(Timestamp::Now, "weather") + /// let query = Timestamp::Now + /// .into_query("weather") /// .add_field("temperature", 82); /// let results = client.query(&query).await?; /// # Ok(()) diff --git a/src/error.rs b/influxdb/src/error.rs similarity index 100% rename from src/error.rs rename to influxdb/src/error.rs diff --git a/src/integrations/serde_integration.rs b/influxdb/src/integrations/serde_integration.rs similarity index 100% rename from src/integrations/serde_integration.rs rename to influxdb/src/integrations/serde_integration.rs diff --git a/src/lib.rs b/influxdb/src/lib.rs similarity index 90% rename from src/lib.rs rename to influxdb/src/lib.rs index 6e45f2a..71474f2 100644 --- a/src/lib.rs +++ b/influxdb/src/lib.rs @@ -10,13 +10,12 @@ //! - Optional Serde Support for Deserialization //! - Running multiple queries in one request (e.g. `SELECT * FROM weather_berlin; SELECT * FROM weather_london`) //! - Authenticated and Unauthenticated Connections -//! - Optional conversion between `Timestamp` and `Chrono::DateTime` via `chrono_timestamps` compilation feature //! - `async`/`await` support +//! - `#[derive(InfluxDbWriteable)]` Derive Macro for Writing / Reading into Structs //! //! ## Planned Features //! //! - Read Query Builder instead of supplying raw queries -//! - `#[derive(InfluxDbReadable)]` and `#[derive(InfluxDbWriteable)]` proc macros //! //! # Quickstart //! @@ -30,6 +29,7 @@ //! //! ```rust,no_run //! use influxdb::{Client, Query, Timestamp}; +//! use influxdb::InfluxDbWriteable; //! //! # #[tokio::main] //! # async fn main() { @@ -40,7 +40,7 @@ //! // Let's write something to InfluxDB. First we're creating a //! // WriteQuery to write some data. //! // This creates a query which writes a new measurement into a series called `weather` -//! let write_query = Query::write_query(Timestamp::Now, "weather") +//! let write_query = Timestamp::Now.into_query("weather") //! .add_field("temperature", 82); //! //! // Submit the query to InfluxDB. @@ -83,7 +83,7 @@ pub use error::Error; pub use query::{ read_query::ReadQuery, write_query::{Type, WriteQuery}, - Query, QueryType, QueryTypes, Timestamp, ValidQuery, + InfluxDbWriteable, Query, QueryType, QueryTypes, Timestamp, ValidQuery, }; #[cfg(feature = "use-serde")] diff --git a/src/query/consts.rs b/influxdb/src/query/consts.rs similarity index 100% rename from src/query/consts.rs rename to influxdb/src/query/consts.rs diff --git a/src/query/line_proto_term.rs b/influxdb/src/query/line_proto_term.rs similarity index 63% rename from src/query/line_proto_term.rs rename to influxdb/src/query/line_proto_term.rs index f9e4d1a..4d35c53 100644 --- a/src/query/line_proto_term.rs +++ b/influxdb/src/query/line_proto_term.rs @@ -8,12 +8,13 @@ lazy_static! { pub static ref COMMAS_SPACES: Regex = Regex::new("[, ]").unwrap(); pub static ref COMMAS_SPACES_EQUALS: Regex = Regex::new("[, =]").unwrap(); pub static ref QUOTES_SLASHES: Regex = Regex::new(r#"["\\]"#).unwrap(); + pub static ref SLASHES: Regex = Regex::new(r#"(\\|,| |=|")"#).unwrap(); } pub enum LineProtoTerm<'a> { Measurement(&'a str), // escape commas, spaces TagKey(&'a str), // escape commas, equals, spaces - TagValue(&'a str), // escape commas, equals, spaces + TagValue(&'a Type), // escape commas, equals, spaces FieldKey(&'a str), // escape commas, equals, spaces FieldValue(&'a Type), // escape quotes, backslashes + quote } @@ -23,8 +24,9 @@ impl LineProtoTerm<'_> { use LineProtoTerm::*; match self { Measurement(x) => Self::escape_any(x, &*COMMAS_SPACES), - TagKey(x) | TagValue(x) | FieldKey(x) => Self::escape_any(x, &*COMMAS_SPACES_EQUALS), + TagKey(x) | FieldKey(x) => Self::escape_any(x, &*COMMAS_SPACES_EQUALS), FieldValue(x) => Self::escape_field_value(x), + TagValue(x) => Self::escape_tag_value(x), } } @@ -42,7 +44,25 @@ impl LineProtoTerm<'_> { Float(v) => v.to_string(), SignedInteger(v) => format!("{}i", v), UnsignedInteger(v) => format!("{}i", v), - Text(v) => format!("\"{}\"", Self::escape_any(v, &*QUOTES_SLASHES)), + Text(v) => format!(r#""{}""#, Self::escape_any(v, &*SLASHES)), + } + } + + fn escape_tag_value(v: &Type) -> String { + use Type::*; + match v { + Boolean(v) => { + if *v { + "true" + } else { + "false" + } + } + .to_string(), + Float(v) => format!(r#""{}""#, v.to_string()), + SignedInteger(v) => format!(r#""{}""#, v), + UnsignedInteger(v) => format!(r#""{}""#, v), + Text(v) => format!(r#""{}""#, Self::escape_any(v, &*SLASHES)), } } @@ -58,6 +78,23 @@ mod test { #[test] fn test() { + assert_eq!( + TagValue(&Type::Text("this is my special string".into())).escape(), + r#""this\ is\ my\ special\ string""# + ); + assert_eq!( + TagValue(&Type::Text("a tag w=i th == tons of escapes".into())).escape(), + r#""a\ tag\ w\=i\ th\ \=\=\ tons\ of\ escapes""# + ); + assert_eq!( + TagValue(&Type::Text("no_escapes".into())).escape(), + r#""no_escapes""# + ); + assert_eq!( + TagValue(&Type::Text("some,commas,here".into())).escape(), + r#""some\,commas\,here""# + ); + assert_eq!(Measurement(r#"wea", ther"#).escape(), r#"wea"\,\ ther"#); assert_eq!(TagKey(r#"locat\ ,=ion"#).escape(), r#"locat\\ \,\=ion"#); @@ -75,7 +112,7 @@ mod test { assert_eq!(FieldValue(&Type::Text("\"".into())).escape(), r#""\"""#); assert_eq!( FieldValue(&Type::Text(r#"locat"\ ,=ion"#.into())).escape(), - r#""locat\"\\ ,=ion""# + r#""locat\"\\\ \,\=ion""# ); } @@ -83,6 +120,6 @@ mod test { fn test_empty_tag_value() { // InfluxDB doesn't support empty tag values. But that's a job // of a calling site to validate an entire write request. - assert_eq!(TagValue("").escape(), ""); + assert_eq!(TagValue(&Type::Text("".into())).escape(), r#""""#); } } diff --git a/src/query/mod.rs b/influxdb/src/query/mod.rs similarity index 84% rename from src/query/mod.rs rename to influxdb/src/query/mod.rs index 3dede5a..4aec2aa 100644 --- a/src/query/mod.rs +++ b/influxdb/src/query/mod.rs @@ -5,8 +5,9 @@ //! //! ```rust //! use influxdb::{Query, Timestamp}; +//! use influxdb::InfluxDbWriteable; //! -//! let write_query = Query::write_query(Timestamp::Now, "measurement") +//! let write_query = Timestamp::Now.into_query("measurement") //! .add_field("field1", 5) //! .add_tag("author", "Gero") //! .build(); @@ -19,15 +20,9 @@ //! assert!(read_query.is_ok()); //! ``` -#[cfg(feature = "chrono_timestamps")] -extern crate chrono; - -#[cfg(feature = "chrono_timestamps")] use chrono::prelude::{DateTime, TimeZone, Utc}; -#[cfg(feature = "chrono_timestamps")] use std::convert::TryInto; -#[cfg(feature = "chrono_timestamps")] pub mod consts; mod line_proto_term; pub mod read_query; @@ -35,10 +30,11 @@ pub mod write_query; use std::fmt; use crate::{Error, ReadQuery, WriteQuery}; - -#[cfg(feature = "chrono_timestamps")] use consts::{MILLIS_PER_SECOND, MINUTES_PER_HOUR, NANOS_PER_MILLI, SECONDS_PER_MINUTE}; +#[cfg(feature = "derive")] +pub use influxdb_derive::InfluxDbWriteable; + #[derive(PartialEq, Debug, Copy, Clone)] pub enum Timestamp { Now, @@ -61,7 +57,6 @@ impl fmt::Display for Timestamp { } } -#[cfg(feature = "chrono_timestamps")] impl Into> for Timestamp { fn into(self) -> DateTime { match self { @@ -92,7 +87,6 @@ impl Into> for Timestamp { } } -#[cfg(feature = "chrono_timestamps")] impl From> for Timestamp where T: TimeZone, @@ -129,11 +123,12 @@ pub trait Query { /// /// ```rust /// use influxdb::{Query, Timestamp}; + /// use influxdb::InfluxDbWriteable; /// - /// let invalid_query = Query::write_query(Timestamp::Now, "measurement").build(); + /// let invalid_query = Timestamp::Now.into_query("measurement").build(); /// assert!(invalid_query.is_err()); /// - /// let valid_query = Query::write_query(Timestamp::Now, "measurement").add_field("myfield1", 11).build(); + /// let valid_query = Timestamp::Now.into_query("measurement").add_field("myfield1", 11).build(); /// assert!(valid_query.is_ok()); /// ``` fn build(&self) -> Result; @@ -141,23 +136,17 @@ pub trait Query { fn get_type(&self) -> QueryType; } -impl dyn Query { - /// Returns a [`WriteQuery`](crate::WriteQuery) builder. - /// - /// # Examples - /// - /// ```rust - /// use influxdb::{Query, Timestamp}; - /// - /// Query::write_query(Timestamp::Now, "measurement"); // Is of type [`WriteQuery`](crate::WriteQuery) - /// ``` - pub fn write_query(timestamp: Timestamp, measurement: S) -> WriteQuery - where - S: Into, - { - WriteQuery::new(timestamp, measurement) +pub trait InfluxDbWriteable { + fn into_query>(self, name: I) -> WriteQuery; +} + +impl InfluxDbWriteable for Timestamp { + fn into_query>(self, name: I) -> WriteQuery { + WriteQuery::new(self, name.into()) } +} +impl dyn Query { /// Returns a [`ReadQuery`](crate::ReadQuery) builder. /// /// # Examples @@ -211,22 +200,16 @@ pub enum QueryType { #[cfg(test)] mod tests { - #[cfg(feature = "chrono_timestamps")] - use std::convert::TryInto; - #[cfg(feature = "chrono_timestamps")] - extern crate chrono; - #[cfg(feature = "chrono_timestamps")] use super::consts::{ MICROS_PER_NANO, MILLIS_PER_SECOND, MINUTES_PER_HOUR, NANOS_PER_MILLI, SECONDS_PER_MINUTE, }; use crate::query::{Timestamp, ValidQuery}; - #[cfg(feature = "chrono_timestamps")] use chrono::prelude::{DateTime, TimeZone, Utc}; + use std::convert::TryInto; #[test] fn test_equality_str() { assert_eq!(ValidQuery::from("hello"), "hello"); } - #[test] fn test_equality_string() { assert_eq!( @@ -234,24 +217,19 @@ mod tests { String::from("hello") ); } - #[test] fn test_format_for_timestamp_now() { assert!(format!("{}", Timestamp::Now) == ""); } - #[test] fn test_format_for_timestamp_else() { assert!(format!("{}", Timestamp::Nanoseconds(100)) == "100"); } - - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_now() { let datetime_from_timestamp: DateTime = Timestamp::Now.into(); assert_eq!(Utc::now().date(), datetime_from_timestamp.date()) } - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_hours() { let datetime_from_timestamp: DateTime = Timestamp::Hours(2).into(); @@ -264,7 +242,6 @@ mod tests { datetime_from_timestamp ) } - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_minutes() { let datetime_from_timestamp: DateTime = Timestamp::Minutes(2).into(); @@ -277,7 +254,6 @@ mod tests { datetime_from_timestamp ) } - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_seconds() { let datetime_from_timestamp: DateTime = Timestamp::Seconds(2).into(); @@ -290,7 +266,6 @@ mod tests { datetime_from_timestamp ) } - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_millis() { let datetime_from_timestamp: DateTime = Timestamp::Milliseconds(2).into(); @@ -299,14 +274,11 @@ mod tests { datetime_from_timestamp ) } - - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_nanos() { let datetime_from_timestamp: DateTime = Timestamp::Nanoseconds(1).into(); assert_eq!(Utc.timestamp_nanos(1), datetime_from_timestamp) } - #[cfg(feature = "chrono_timestamps")] #[test] fn test_chrono_datetime_from_timestamp_micros() { let datetime_from_timestamp: DateTime = Timestamp::Microseconds(1).into(); @@ -315,8 +287,6 @@ mod tests { datetime_from_timestamp ) } - - #[cfg(feature = "chrono_timestamps")] #[test] fn test_timestamp_from_chrono_date() { let timestamp_from_datetime: Timestamp = Utc.ymd(1970, 1, 1).and_hms(0, 0, 1).into(); diff --git a/src/query/read_query.rs b/influxdb/src/query/read_query.rs similarity index 100% rename from src/query/read_query.rs rename to influxdb/src/query/read_query.rs diff --git a/src/query/write_query.rs b/influxdb/src/query/write_query.rs similarity index 75% rename from src/query/write_query.rs rename to influxdb/src/query/write_query.rs index 74bef11..b66c747 100644 --- a/src/query/write_query.rs +++ b/influxdb/src/query/write_query.rs @@ -7,23 +7,21 @@ use crate::query::{QueryType, ValidQuery}; use crate::{Error, Query, Timestamp}; use std::fmt::{Display, Formatter}; -// todo: batch write queries - -pub trait WriteField { - fn add_to_fields(self, tag: String, fields: &mut Vec<(String, Type)>); +pub trait WriteType { + fn add_to(self, tag: String, fields_or_tags: &mut Vec<(String, Type)>); } -impl> WriteField for T { - fn add_to_fields(self, tag: String, fields: &mut Vec<(String, Type)>) { +impl> WriteType for T { + fn add_to(self, tag: String, fields_or_tags: &mut Vec<(String, Type)>) { let val: Type = self.into(); - fields.push((tag, val)); + fields_or_tags.push((tag, val)); } } -impl> WriteField for Option { - fn add_to_fields(self, tag: String, fields: &mut Vec<(String, Type)>) { +impl> WriteType for Option { + fn add_to(self, tag: String, fields_or_tags: &mut Vec<(String, Type)>) { if let Some(val) = self { - val.add_to_fields(tag, fields); + val.add_to(tag, fields_or_tags); } } } @@ -31,7 +29,7 @@ impl> WriteField for Option { /// Internal Representation of a Write query that has not yet been built pub struct WriteQuery { fields: Vec<(String, Type)>, - tags: Vec<(String, String)>, + tags: Vec<(String, Type)>, measurement: String, timestamp: Timestamp, } @@ -56,15 +54,16 @@ impl WriteQuery { /// /// ```rust /// use influxdb::{Query, Timestamp}; + /// use influxdb::InfluxDbWriteable; /// - /// Query::write_query(Timestamp::Now, "measurement").add_field("field1", 5).build(); + /// Timestamp::Now.into_query("measurement").add_field("field1", 5).build(); /// ``` - pub fn add_field(mut self, tag: S, value: F) -> Self + pub fn add_field(mut self, field: S, value: F) -> Self where S: Into, - F: WriteField, + F: WriteType, { - value.add_to_fields(tag.into(), &mut self.fields); + value.add_to(field.into(), &mut self.fields); self } @@ -77,17 +76,18 @@ impl WriteQuery { /// /// ```rust /// use influxdb::{Query, Timestamp}; + /// use influxdb::InfluxDbWriteable; /// - /// Query::write_query(Timestamp::Now, "measurement") + /// Timestamp::Now + /// .into_query("measurement") /// .add_tag("field1", 5); // calling `.build()` now would result in a `Err(Error::InvalidQueryError)` /// ``` pub fn add_tag(mut self, tag: S, value: I) -> Self where S: Into, - I: Into, + I: WriteType, { - let val: Type = value.into(); - self.tags.push((tag.into(), val.to_string())); + value.add_to(tag.into(), &mut self.tags); self } @@ -148,6 +148,14 @@ impl From<&str> for Type { Type::Text(b.into()) } } +impl From<&T> for Type +where + T: Copy + Into, +{ + fn from(t: &T) -> Self { + (*t).into() + } +} impl Query for WriteQuery { fn build(&self) -> Result { @@ -205,18 +213,21 @@ impl Query for WriteQuery { #[cfg(test)] mod tests { - use crate::query::{Query, Timestamp}; + use crate::query::{InfluxDbWriteable, Query, Timestamp}; #[test] fn test_write_builder_empty_query() { - let query = Query::write_query(Timestamp::Hours(5), "marina_3").build(); + let query = Timestamp::Hours(5) + .into_query("marina_3".to_string()) + .build(); assert!(query.is_err(), "Query was not empty"); } #[test] fn test_write_builder_single_field() { - let query = Query::write_query(Timestamp::Hours(11), "weather") + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) .add_field("temperature", 82) .build(); @@ -226,7 +237,8 @@ mod tests { #[test] fn test_write_builder_multiple_fields() { - let query = Query::write_query(Timestamp::Hours(11), "weather") + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) .add_field("temperature", 82) .add_field("wind_strength", 3.7) .build(); @@ -240,9 +252,10 @@ mod tests { #[test] fn test_write_builder_optional_fields() { - let query = Query::write_query(Timestamp::Hours(11), "weather") - .add_field("temperature", Some(82u64)) - .add_field("wind_strength", >::None) + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) + .add_field("temperature", 82u64) + .add_tag("wind_strength", >::None) .build(); assert!(query.is_ok(), "Query was empty"); @@ -251,7 +264,8 @@ mod tests { #[test] fn test_write_builder_only_tags() { - let query = Query::write_query(Timestamp::Hours(11), "weather") + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) .add_tag("season", "summer") .build(); @@ -260,7 +274,8 @@ mod tests { #[test] fn test_write_builder_full_query() { - let query = Query::write_query(Timestamp::Hours(11), "weather") + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) .add_field("temperature", 82) .add_tag("location", "us-midwest") .add_tag("season", "summer") @@ -269,7 +284,7 @@ mod tests { assert!(query.is_ok(), "Query was empty"); assert_eq!( query.unwrap(), - "weather,location=us-midwest,season=summer temperature=82i 11" + r#"weather,location="us-midwest",season="summer" temperature=82i 11"# ); } @@ -277,7 +292,8 @@ mod tests { fn test_correct_query_type() { use crate::query::QueryType; - let query = Query::write_query(Timestamp::Hours(11), "weather") + let query = Timestamp::Hours(11) + .into_query("weather".to_string()) .add_field("temperature", 82) .add_tag("location", "us-midwest") .add_tag("season", "summer"); @@ -287,18 +303,21 @@ mod tests { #[test] fn test_escaping() { - let query = Query::write_query(Timestamp::Hours(11), "wea, ther=") + let query = Timestamp::Hours(11) + .into_query("wea, ther=") .add_field("temperature", 82) .add_field("\"temp=era,t ure\"", r#"too"\\hot"#) .add_field("float", 82.0) .add_tag("location", "us-midwest") - .add_tag("loc, =\"ation", "us, \"mid=west\"") + .add_tag("loc, =\"ation", r#"us, "mid=west""#) .build(); assert!(query.is_ok(), "Query was empty"); + let query_res = query.unwrap().get(); + #[allow(clippy::print_literal)] assert_eq!( - query.unwrap().get(), - r#"wea\,\ ther=,location=us-midwest,loc\,\ \="ation=us\,\ "mid\=west" temperature=82i,"temp\=era\,t\ ure"="too\"\\\\hot",float=82 11"# + query_res, + r#"wea\,\ ther=,location="us-midwest",loc\,\ \="ation="us\,\ \"mid\=west\"" temperature=82i,"temp\=era\,t\ ure"="too\"\\\\hot",float=82 11"# ); } } diff --git a/influxdb/tests/derive_integration_tests.rs b/influxdb/tests/derive_integration_tests.rs new file mode 100644 index 0000000..afede39 --- /dev/null +++ b/influxdb/tests/derive_integration_tests.rs @@ -0,0 +1,108 @@ +#[path = "./utilities.rs"] +mod utilities; + +#[cfg(feature = "derive")] +use influxdb::InfluxDbWriteable; + +use chrono::{DateTime, Utc}; +use influxdb::{Query, Timestamp}; + +#[cfg(feature = "use-serde")] +use serde::Deserialize; + +use utilities::{assert_result_ok, create_client, create_db, delete_db, run_test}; + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "derive", derive(InfluxDbWriteable))] +#[cfg_attr(feature = "use-serde", derive(Deserialize))] +struct WeatherReading { + time: DateTime, + humidity: i32, + #[tag] + wind_strength: Option, +} + +#[test] +fn test_build_query() { + let weather_reading = WeatherReading { + time: Timestamp::Hours(1).into(), + humidity: 30, + wind_strength: Some(5), + }; + let query = weather_reading + .into_query("weather_reading") + .build() + .unwrap(); + assert_eq!( + query.get(), + "weather_reading,wind_strength=\"5\" humidity=30i 3600000000000" + ); +} + +#[cfg(feature = "derive")] +/// INTEGRATION TEST +/// +/// This integration tests that writing data and retrieving the data again is working +#[tokio::test] +async fn test_derive_simple_write() { + const TEST_NAME: &str = "test_derive_simple_write"; + + run_test( + || async move { + create_db(TEST_NAME).await.expect("could not setup db"); + let client = create_client(TEST_NAME); + let weather_reading = WeatherReading { + time: Timestamp::Now.into(), + humidity: 30, + wind_strength: Some(5), + }; + let query = weather_reading.into_query("weather_reading"); + let result = client.query(&query).await; + assert!(result.is_ok(), "unable to insert into db"); + }, + || async move { + delete_db(TEST_NAME).await.expect("could not clean up db"); + }, + ) + .await; +} + +/// INTEGRATION TEST +/// +/// This integration tests that writing data and retrieving the data again is working +#[cfg(feature = "derive")] +#[cfg(feature = "use-serde")] +#[tokio::test] +async fn test_write_and_read_option() { + const TEST_NAME: &str = "test_write_and_read_option"; + + run_test( + || async move { + create_db(TEST_NAME).await.expect("could not setup db"); + let client = create_client(TEST_NAME); + let weather_reading = WeatherReading { + time: Timestamp::Hours(11).into(), + humidity: 30, + wind_strength: None, + }; + let write_result = client + .query(&weather_reading.into_query("weather_reading".to_string())) + .await; + assert_result_ok(&write_result); + let query = + Query::raw_read_query("SELECT time, humidity, wind_strength FROM weather_reading"); + let result = client.json_query(query).await.and_then(|mut db_result| { + println!("{:?}", db_result); + db_result.deserialize_next::() + }); + assert_result_ok(&result); + let result = result.unwrap(); + assert_eq!(result.series[0].values[0].humidity, 30); + assert_eq!(result.series[0].values[0].wind_strength, None); + }, + || async move { + delete_db(TEST_NAME).await.expect("could not clean up db"); + }, + ) + .await; +} diff --git a/tests/integration_tests.rs b/influxdb/tests/integration_tests.rs similarity index 79% rename from tests/integration_tests.rs rename to influxdb/tests/integration_tests.rs index 6a0e386..d69447e 100644 --- a/tests/integration_tests.rs +++ b/influxdb/tests/integration_tests.rs @@ -1,62 +1,15 @@ extern crate influxdb; -use futures::prelude::*; +#[path = "./utilities.rs"] +mod utilities; +use utilities::{ + assert_result_err, assert_result_ok, create_client, create_db, delete_db, run_test, +}; + +use influxdb::InfluxDbWriteable; use influxdb::{Client, Error, Query, Timestamp}; -use std::panic::{AssertUnwindSafe, UnwindSafe}; use tokio; -fn assert_result_err(result: &Result) { - result.as_ref().expect_err("assert_result_err failed"); -} - -fn assert_result_ok(result: &Result) { - result.as_ref().expect("assert_result_ok failed"); -} - -fn create_client(db_name: T) -> Client -where - T: Into, -{ - Client::new("http://localhost:8086", db_name) -} - -async fn create_db(name: T) -> Result -where - T: Into, -{ - let test_name = name.into(); - let query = format!("CREATE DATABASE {}", test_name); - create_client(test_name) - .query(&Query::raw_read_query(query)) - .await -} - -async fn delete_db(name: T) -> Result -where - T: Into, -{ - let test_name = name.into(); - let query = format!("DROP DATABASE {}", test_name); - create_client(test_name) - .query(&Query::raw_read_query(query)) - .await -} - -async fn run_test(test_fn: F, teardown: T) -where - F: FnOnce() -> Fut1 + UnwindSafe, - T: FnOnce() -> Fut2, - Fut1: Future, - Fut2: Future, -{ - let test_result = AssertUnwindSafe(test_fn()).catch_unwind().await; - AssertUnwindSafe(teardown()) - .catch_unwind() - .await - .expect("failed teardown"); - test_result.expect("failed test"); -} - /// INTEGRATION TEST /// /// This test case tests whether the InfluxDB server can be connected to and gathers info about it @@ -70,7 +23,7 @@ async fn test_ping_influx_db() { assert!(!build.is_empty(), "Build should not be empty"); assert!(!version.is_empty(), "Build should not be empty"); - println!("build: {} version: {}", build, version); + println!("build: {} version: {}", build, version); } /// INTEGRATION TEST @@ -112,8 +65,9 @@ async fn test_authed_write_and_read() { let client = Client::new("http://localhost:9086", TEST_NAME).with_auth("admin", "password"); - let write_query = - Query::write_query(Timestamp::Hours(11), "weather").add_field("temperature", 82); + let write_query = Timestamp::Hours(11) + .into_query("weather") + .add_field("temperature", 82); let write_result = client.query(&write_query).await; assert_result_ok(&write_result); @@ -158,8 +112,9 @@ async fn test_wrong_authed_write_and_read() { let client = Client::new("http://localhost:9086", TEST_NAME).with_auth("wrong_user", "password"); - let write_query = - Query::write_query(Timestamp::Hours(11), "weather").add_field("temperature", 82); + let write_query = Timestamp::Hours(11) + .into_query("weather") + .add_field("temperature", 82); let write_result = client.query(&write_query).await; assert_result_err(&write_result); match write_result { @@ -224,8 +179,9 @@ async fn test_non_authed_write_and_read() { .await .expect("could not setup db"); let non_authed_client = Client::new("http://localhost:9086", TEST_NAME); - let write_query = - Query::write_query(Timestamp::Hours(11), "weather").add_field("temperature", 82); + let write_query = Timestamp::Hours(11) + .into_query("weather") + .add_field("temperature", 82); let write_result = non_authed_client.query(&write_query).await; assert_result_err(&write_result); match write_result { @@ -271,8 +227,9 @@ async fn test_write_and_read_field() { || async move { create_db(TEST_NAME).await.expect("could not setup db"); let client = create_client(TEST_NAME); - let write_query = - Query::write_query(Timestamp::Hours(11), "weather").add_field("temperature", 82); + let write_query = Timestamp::Hours(11) + .into_query("weather") + .add_field("temperature", 82); let write_result = client.query(&write_query).await; assert_result_ok(&write_result); @@ -308,7 +265,8 @@ async fn test_write_and_read_option() { let client = create_client(TEST_NAME); // Todo: Convert this to derive based insert for easier comparison of structs - let write_query = Query::write_query(Timestamp::Hours(11), "weather") + let write_query = Timestamp::Hours(11) + .into_query("weather") .add_field("temperature", 82) .add_field("wind_strength", >::None); let write_result = client.query(&write_query).await; @@ -360,39 +318,37 @@ async fn test_json_query() { const TEST_NAME: &str = "test_json_query"; run_test( - || { - async move { - create_db(TEST_NAME).await.expect("could not setup db"); + || async move { + create_db(TEST_NAME).await.expect("could not setup db"); - let client = create_client(TEST_NAME); + let client = create_client(TEST_NAME); - // todo: implement deriving so objects can easily be placed in InfluxDB - let write_query = Query::write_query(Timestamp::Hours(11), "weather") - .add_field("temperature", 82); - let write_result = client.query(&write_query).await; - assert_result_ok(&write_result); + let write_query = Timestamp::Hours(11) + .into_query("weather") + .add_field("temperature", 82); + let write_result = client.query(&write_query).await; + assert_result_ok(&write_result); - #[derive(Deserialize, Debug, PartialEq)] - struct Weather { - time: String, - temperature: i32, - } + #[derive(Deserialize, Debug, PartialEq)] + struct Weather { + time: String, + temperature: i32, + } - let query = Query::raw_read_query("SELECT * FROM weather"); - let result = client - .json_query(query) - .await - .and_then(|mut db_result| db_result.deserialize_next::()); - assert_result_ok(&result); + let query = Query::raw_read_query("SELECT * FROM weather"); + let result = client + .json_query(query) + .await + .and_then(|mut db_result| db_result.deserialize_next::()); + assert_result_ok(&result); - assert_eq!( - result.unwrap().series[0].values[0], - Weather { - time: "1970-01-01T11:00:00Z".to_string(), - temperature: 82 - } - ); - } + assert_eq!( + result.unwrap().series[0].values[0], + Weather { + time: "1970-01-01T11:00:00Z".to_string(), + temperature: 82 + } + ); }, || async move { delete_db(TEST_NAME).await.expect("could not clean up db"); @@ -417,11 +373,14 @@ async fn test_json_query_vec() { create_db(TEST_NAME).await.expect("could not setup db"); let client = create_client(TEST_NAME); - let write_query1 = Query::write_query(Timestamp::Hours(11), "temperature_vec") + let write_query1 = Timestamp::Hours(11) + .into_query("temperature_vec") .add_field("temperature", 16); - let write_query2 = Query::write_query(Timestamp::Hours(12), "temperature_vec") + let write_query2 = Timestamp::Hours(12) + .into_query("temperature_vec") .add_field("temperature", 17); - let write_query3 = Query::write_query(Timestamp::Hours(13), "temperature_vec") + let write_query3 = Timestamp::Hours(13) + .into_query("temperature_vec") .add_field("temperature", 18); let _write_result = client.query(&write_query1).await; @@ -476,10 +435,12 @@ async fn test_serde_multi_query() { } let client = create_client(TEST_NAME); - let write_query = Query::write_query(Timestamp::Hours(11), "temperature") + let write_query = Timestamp::Hours(11) + .into_query("temperature") .add_field("temperature", 16); - let write_query2 = - Query::write_query(Timestamp::Hours(11), "humidity").add_field("humidity", 69); + let write_query2 = Timestamp::Hours(11) + .into_query("humidity") + .add_field("humidity", 69); let write_result = client.query(&write_query).await; let write_result2 = client.query(&write_query2).await; diff --git a/influxdb/tests/utilities.rs b/influxdb/tests/utilities.rs new file mode 100644 index 0000000..11a498a --- /dev/null +++ b/influxdb/tests/utilities.rs @@ -0,0 +1,56 @@ +use futures::prelude::*; +use influxdb::{Client, Error, Query}; +use std::panic::{AssertUnwindSafe, UnwindSafe}; + +#[allow(dead_code)] +pub fn assert_result_err(result: &Result) { + result.as_ref().expect_err("assert_result_err failed"); +} + +pub fn assert_result_ok(result: &Result) { + result.as_ref().expect("assert_result_ok failed"); +} + +pub fn create_client(db_name: T) -> Client +where + T: Into, +{ + Client::new("http://localhost:8086", db_name) +} + +pub async fn create_db(name: T) -> Result +where + T: Into, +{ + let test_name = name.into(); + let query = format!("CREATE DATABASE {}", test_name); + create_client(test_name) + .query(&Query::raw_read_query(query)) + .await +} + +pub async fn delete_db(name: T) -> Result +where + T: Into, +{ + let test_name = name.into(); + let query = format!("DROP DATABASE {}", test_name); + create_client(test_name) + .query(&Query::raw_read_query(query)) + .await +} + +pub async fn run_test(test_fn: F, teardown: T) +where + F: FnOnce() -> Fut1 + UnwindSafe, + T: FnOnce() -> Fut2, + Fut1: Future, + Fut2: Future, +{ + let test_result = AssertUnwindSafe(test_fn()).catch_unwind().await; + AssertUnwindSafe(teardown()) + .catch_unwind() + .await + .expect("failed teardown"); + test_result.expect("failed test"); +} diff --git a/influxdb_derive/Cargo.toml b/influxdb_derive/Cargo.toml new file mode 100644 index 0000000..df9939e --- /dev/null +++ b/influxdb_derive/Cargo.toml @@ -0,0 +1,25 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "influxdb_derive" +version = "0.0.1" +authors = ["Gero Gerke <11deutron11@gmail.com>"] +edition = "2018" +description = "InfluxDB Driver for Rust - Derive" +keywords = ["influxdb", "database", "influx", "derive"] +license = "MIT" +readme = "README.md" +include = ["src/**/*", "tests/**/*", "Cargo.toml", "LICENSE"] +repository = "https://github.com/Empty2k12/influxdb-rust" + +[lib] +proc-macro = true + +[badges] +travis-ci = { repository = "Empty2k12/influxdb-rust", branch = "master" } +coveralls = { repository = "Empty2k12/influxdb-rust", branch = "master", service = "github" } + +[dependencies] +proc-macro2 = "1.0.5" +quote = "1.0.2" +syn = { version = "1.0.5", features = ["extra-traits", "full"] } diff --git a/influxdb_derive/LICENSE b/influxdb_derive/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/influxdb_derive/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/influxdb_derive/README.md b/influxdb_derive/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/influxdb_derive/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/influxdb_derive/src/lib.rs b/influxdb_derive/src/lib.rs new file mode 100644 index 0000000..2bfe464 --- /dev/null +++ b/influxdb_derive/src/lib.rs @@ -0,0 +1,17 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; + +mod writeable; +use writeable::expand_writeable; + +fn krate() -> TokenStream2 { + quote!(::influxdb) +} + +#[proc_macro_derive(InfluxDbWriteable, attributes(tag))] +pub fn derive_writeable(tokens: TokenStream) -> TokenStream { + expand_writeable(tokens) +} diff --git a/influxdb_derive/src/writeable.rs b/influxdb_derive/src/writeable.rs new file mode 100644 index 0000000..f42c333 --- /dev/null +++ b/influxdb_derive/src/writeable.rs @@ -0,0 +1,69 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Field, Fields, Ident, ItemStruct}; + +struct WriteableField { + ident: Ident, + is_tag: bool, +} + +impl From for WriteableField { + fn from(field: Field) -> WriteableField { + let ident = field.ident.expect("fields without ident are not supported"); + let is_tag = field.attrs.iter().any(|attr| { + attr.path + .segments + .iter() + .last() + .map(|seg| seg.ident.to_string()) + .unwrap_or_default() + == "tag" + }); + WriteableField { ident, is_tag } + } +} + +pub fn expand_writeable(tokens: TokenStream) -> TokenStream { + let krate = super::krate(); + + let input = parse_macro_input!(tokens as ItemStruct); + let ident = input.ident; + let generics = input.generics; + + let time_field = format_ident!("time"); + #[allow(clippy::cmp_owned)] // that's not how idents work clippy + let fields: Vec = match input.fields { + Fields::Named(fields) => fields + .named + .into_iter() + .map(WriteableField::from) + .filter(|field| field.ident.to_string() != time_field.to_string()) + .map(|field| { + let ident = field.ident; + #[allow(clippy::match_bool)] + match field.is_tag { + true => quote!(query.add_tag(stringify!(#ident), self.#ident)), + false => quote!(query.add_field(stringify!(#ident), self.#ident)), + } + }) + .collect(), + _ => panic!("a struct without named fields is not supported"), + }; + + let output = quote! { + impl #generics #krate::InfluxDbWriteable for #ident #generics + { + fn into_query>(self, name : I) -> #krate::WriteQuery + { + let timestamp : #krate::Timestamp = self.#time_field.into(); + let mut query = timestamp.into_query(name); + #( + query = #fields; + )* + query + } + } + }; + output.into() +}