diff --git a/Cargo.toml b/Cargo.toml index d33b2172..495455c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ resolver = "2" [workspace.package] authors = ["Gero Gerke ", "Dominic "] edition = "2021" -rust-version = "1.67.1" +rust-version = "1.70" license = "MIT" repository = "https://github.com/influxdb-rs/influxdb-rust" diff --git a/README.md b/README.md index 5a329b1d..23154df0 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Build with Rust - - Minimum Rust Version: 1.67.1 + + Minimum Rust Version: 1.70.0

@@ -75,17 +75,19 @@ async fn main() -> Result<(), Error> { // Let's write some data into a measurement called `weather` let weather_readings = vec![ WeatherReading { - time: Timestamp::Hours(1).into(), + time: Timestamp::Hours(1).try_into().unwrap(), humidity: 30, wind_direction: String::from("north"), } - .into_query("weather"), + .try_into_query("weather") + .unwrap(), WeatherReading { - time: Timestamp::Hours(2).into(), + time: Timestamp::Hours(2).try_into().unwrap(), humidity: 40, wind_direction: String::from("west"), } - .into_query("weather"), + .try_into_query("weather") + .unwrap(), ]; client.query(weather_readings).await?; @@ -129,7 +131,7 @@ To communicate with InfluxDB, you can choose the HTTP backend to be used configu @ 2020-2024 Gero Gerke, msrd0 and [contributors]. [contributors]: https://github.com/influxdb-rs/influxdb-rust/graphs/contributors - [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGzJ_QpW55zB1G0S-TER-rIfLG2gXv8EYBG3jG1nuXXn-kdx-YXKEG8LHWNBBuXgSGz-2Lrx4E_kTG0bJiXb6A8zNG9GhXhvU8L0xYWSBgmhpbmZsdXhkYmUwLjcuMg + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGzJ_QpW55zB1G0S-TER-rIfLG2gXv8EYBG3jG1nuXXn-kdx-YXKEG5esg8JWCUnDGygXCh47ngu0G4kPgAyV809_G2pbKPyN9jeVYWSBgmhpbmZsdXhkYmUwLjcuMg [__link0]: https://github.com/influxdb-rs/influxdb-rust/blob/main/CONTRIBUTING.md [__link1]: https://github.com/influxdb-rs/influxdb-rust/blob/main/CODE_OF_CONDUCT.md [__link2]: https://github.com/influxdb-rs/influxdb-rust/blob/main/CHANGELOG.md diff --git a/benches/client.rs b/benches/client.rs index e07bd1b2..b23203a7 100644 --- a/benches/client.rs +++ b/benches/client.rs @@ -1,7 +1,5 @@ use chrono::{DateTime, Utc}; -use influxdb::Error; -use influxdb::InfluxDbWriteable; -use influxdb::{Client, ReadQuery}; +use influxdb::{Client, Error, InfluxDbWriteable, ReadQuery}; use std::sync::Arc; use std::time::Instant; use tokio::sync::mpsc::unbounded_channel; diff --git a/influxdb/src/client/mod.rs b/influxdb/src/client/mod.rs index ed9e5dc8..3a21d59b 100644 --- a/influxdb/src/client/mod.rs +++ b/influxdb/src/client/mod.rs @@ -22,8 +22,7 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use crate::query::QueryType; -use crate::Error; -use crate::Query; +use crate::{Error, Query}; #[derive(Clone)] /// Internal Representation of a Client @@ -188,21 +187,21 @@ impl Client { /// # Examples /// /// ```rust,no_run - /// use influxdb::{Client, Query, Timestamp}; - /// use influxdb::InfluxDbWriteable; + /// use influxdb::{Client, InfluxDbWriteable, Query, Timestamp}; /// use std::time::{SystemTime, UNIX_EPOCH}; /// /// # #[tokio::main] /// # async fn main() -> Result<(), influxdb::Error> { /// let start = SystemTime::now(); /// let since_the_epoch = start - /// .duration_since(UNIX_EPOCH) - /// .expect("Time went backwards") - /// .as_millis(); + /// .duration_since(UNIX_EPOCH) + /// .expect("Time went backwards") + /// .as_millis(); /// /// let client = Client::new("http://localhost:8086", "test"); /// let query = Timestamp::Milliseconds(since_the_epoch) - /// .into_query("weather") + /// .try_into_query("weather") + /// .unwrap() /// .add_field("temperature", 82); /// let results = client.query(query).await?; /// diff --git a/influxdb/src/error.rs b/influxdb/src/error.rs index 5f89e8f9..e9adb6e3 100644 --- a/influxdb/src/error.rs +++ b/influxdb/src/error.rs @@ -33,3 +33,15 @@ pub enum Error { /// Error happens when HTTP request fails ConnectionError { error: String }, } + +#[cfg(feature = "chrono")] +#[derive(Clone, Copy, Debug, Error)] +#[error("The timestamp is too large to fit into an i64.")] +pub struct TimestampTooLargeError(pub(crate) ()); + +#[cfg(any(feature = "chrono", feature = "time"))] +#[derive(Clone, Copy, Debug, Error)] +pub enum TimeTryFromError { + TimeError(#[source] T), + IntError(#[source] I), +} diff --git a/influxdb/src/integrations/serde_integration/mod.rs b/influxdb/src/integrations/serde_integration/mod.rs index e4000be2..42a69e95 100644 --- a/influxdb/src/integrations/serde_integration/mod.rs +++ b/influxdb/src/integrations/serde_integration/mod.rs @@ -33,8 +33,7 @@ //! .series //! .into_iter() //! .map(|mut city_series| { -//! let city_name = -//! city_series.name.split("_").collect::>().remove(2); +//! let city_name = city_series.name.split("_").collect::>().remove(2); //! Weather { //! weather: city_series.values.remove(0), //! city_name: city_name.to_string(), @@ -50,7 +49,8 @@ mod de; use serde::de::DeserializeOwned; use serde_derive::Deserialize; -use crate::{client::check_status, Client, Error, Query, ReadQuery}; +use crate::client::check_status; +use crate::{Client, Error, Query, ReadQuery}; #[derive(Deserialize)] #[doc(hidden)] diff --git a/influxdb/src/lib.rs b/influxdb/src/lib.rs index 4661fd5c..a070cb69 100644 --- a/influxdb/src/lib.rs +++ b/influxdb/src/lib.rs @@ -16,7 +16,6 @@ //! # Quickstart //! //! Add the following to your `Cargo.toml` -//! #![doc = cargo_toml!(indent="", "derive")] //! //! For an example with using Serde deserialization, please refer to [serde_integration](crate::integrations::serde_integration) @@ -41,17 +40,19 @@ //! // Let's write some data into a measurement called `weather` //! let weather_readings = vec![ //! WeatherReading { -//! time: Timestamp::Hours(1).into(), +//! time: Timestamp::Hours(1).try_into().unwrap(), //! humidity: 30, //! wind_direction: String::from("north"), //! } -//! .into_query("weather"), +//! .try_into_query("weather") +//! .unwrap(), //! WeatherReading { -//! time: Timestamp::Hours(2).into(), +//! time: Timestamp::Hours(2).try_into().unwrap(), //! humidity: 40, //! wind_direction: String::from("west"), //! } -//! .into_query("weather"), +//! .try_into_query("weather") +//! .unwrap(), //! ]; //! //! client.query(weather_readings).await?; @@ -123,11 +124,9 @@ mod query; pub use client::Client; pub use error::Error; -pub use query::{ - read_query::ReadQuery, - write_query::{Type, WriteQuery}, - InfluxDbWriteable, Query, QueryType, Timestamp, ValidQuery, -}; +pub use query::read_query::ReadQuery; +pub use query::write_query::{Type, WriteQuery}; +pub use query::{InfluxDbWriteable, Query, QueryType, Timestamp, ValidQuery}; #[cfg(feature = "serde")] pub mod integrations { diff --git a/influxdb/src/query/mod.rs b/influxdb/src/query/mod.rs index 19a73e39..1736eaf3 100644 --- a/influxdb/src/query/mod.rs +++ b/influxdb/src/query/mod.rs @@ -4,18 +4,18 @@ //! # Examples //! //! ```rust -//! use influxdb::{ReadQuery, Query as _, Timestamp}; -//! use influxdb::InfluxDbWriteable; +//! use influxdb::{InfluxDbWriteable, Query as _, ReadQuery, Timestamp}; //! -//! let write_query = Timestamp::Nanoseconds(0).into_query("measurement") +//! let write_query = Timestamp::Nanoseconds(0) +//! .try_into_query("measurement") +//! .unwrap() //! .add_field("field1", 5) //! .add_tag("author", "Gero") //! .build(); //! //! assert!(write_query.is_ok()); //! -//! let read_query = ReadQuery::new("SELECT * FROM weather") -//! .build(); +//! let read_query = ReadQuery::new("SELECT * FROM weather").build(); //! //! assert!(read_query.is_ok()); //! ``` @@ -24,6 +24,7 @@ pub mod consts; mod line_proto_term; pub mod read_query; pub mod write_query; +use std::convert::Infallible; use std::fmt; use crate::{Error, WriteQuery}; @@ -70,48 +71,55 @@ impl fmt::Display for Timestamp { } #[cfg(feature = "chrono")] -impl From for chrono::DateTime { - fn from(ts: Timestamp) -> chrono::DateTime { +impl TryFrom for chrono::DateTime { + type Error = >::Error; + + fn try_from(ts: Timestamp) -> Result { use chrono::TimeZone as _; - chrono::Utc.timestamp_nanos(ts.nanos() as i64) + Ok(chrono::Utc.timestamp_nanos(ts.nanos().try_into()?)) } } #[cfg(feature = "chrono")] -impl From> for Timestamp -where - T: chrono::TimeZone, -{ - fn from(date_time: chrono::DateTime) -> Self { - Timestamp::Nanoseconds(date_time.timestamp_nanos_opt().unwrap() as u128) - } -} +impl TryFrom> for Timestamp { + type Error = crate::error::TimeTryFromError< + crate::error::TimestampTooLargeError, + >::Error, + >; -#[cfg(feature = "time")] -impl From for time::UtcDateTime { - fn from(value: Timestamp) -> Self { - time::UtcDateTime::from_unix_timestamp_nanos(value.nanos() as i128).unwrap() + fn try_from(dt: chrono::DateTime) -> Result { + // unfortunately chrono doesn't give us the nanos as i128, so we have to error + // if it doesn't fit and then cast the i64 to u128 anyways + let nanos = dt + .timestamp_nanos_opt() + .ok_or(Self::Error::TimeError( + crate::error::TimestampTooLargeError(()), + ))? + .try_into() + .map_err(Self::Error::IntError)?; + Ok(Self::Nanoseconds(nanos)) } } #[cfg(feature = "time")] -impl From for Timestamp { - fn from(value: time::UtcDateTime) -> Self { - Timestamp::Nanoseconds(value.unix_timestamp_nanos() as u128) - } -} +impl TryFrom for time::UtcDateTime { + type Error = + crate::error::TimeTryFromError>::Error>; -#[cfg(feature = "time")] -impl From for time::OffsetDateTime { - fn from(value: Timestamp) -> Self { - time::OffsetDateTime::from_unix_timestamp_nanos(value.nanos() as i128).unwrap() + fn try_from(value: Timestamp) -> Result { + let nanos = value.nanos().try_into().map_err(Self::Error::IntError)?; + time::UtcDateTime::from_unix_timestamp_nanos(nanos).map_err(Self::Error::TimeError) } } #[cfg(feature = "time")] -impl From for Timestamp { - fn from(value: time::OffsetDateTime) -> Self { - Timestamp::Nanoseconds(value.unix_timestamp_nanos() as u128) +impl TryFrom for Timestamp { + type Error = >::Error; + + fn try_from(value: time::UtcDateTime) -> Result { + Ok(Timestamp::Nanoseconds( + value.unix_timestamp_nanos().try_into()?, + )) } } @@ -123,13 +131,19 @@ pub trait Query { /// # Examples /// /// ```rust - /// use influxdb::{Query, Timestamp}; - /// use influxdb::InfluxDbWriteable; + /// use influxdb::{InfluxDbWriteable, Query, Timestamp}; /// - /// let invalid_query = Timestamp::Nanoseconds(0).into_query("measurement").build(); + /// let invalid_query = Timestamp::Nanoseconds(0) + /// .try_into_query("measurement") + /// .unwrap() + /// .build(); /// assert!(invalid_query.is_err()); /// - /// let valid_query = Timestamp::Nanoseconds(0).into_query("measurement").add_field("myfield1", 11).build(); + /// let valid_query = Timestamp::Nanoseconds(0) + /// .try_into_query("measurement") + /// .unwrap() + /// .add_field("myfield1", 11) + /// .build(); /// assert!(valid_query.is_ok()); /// ``` fn build(&self) -> Result; @@ -141,15 +155,21 @@ pub trait Query { /// # Examples /// /// ```rust - /// use influxdb::{Query, Timestamp}; - /// use influxdb::InfluxDbWriteable; + /// use influxdb::{InfluxDbWriteable, Query, Timestamp}; /// /// let use_v2 = true; /// - /// let invalid_query = Timestamp::Nanoseconds(0).into_query("measurement").build_with_opts(use_v2); + /// let invalid_query = Timestamp::Nanoseconds(0) + /// .try_into_query("measurement") + /// .unwrap() + /// .build_with_opts(use_v2); /// assert!(invalid_query.is_err()); /// - /// let valid_query = Timestamp::Nanoseconds(0).into_query("measurement").add_field("myfield1", 11).build_with_opts(use_v2); + /// let valid_query = Timestamp::Nanoseconds(0) + /// .try_into_query("measurement") + /// .unwrap() + /// .add_field("myfield1", 11) + /// .build_with_opts(use_v2); /// assert!(valid_query.is_ok()); /// ``` fn build_with_opts(&self, use_v2: bool) -> Result; @@ -186,12 +206,16 @@ impl Query for Box { } pub trait InfluxDbWriteable { - fn into_query>(self, name: I) -> WriteQuery; + type Error; + + fn try_into_query>(self, name: I) -> Result; } impl InfluxDbWriteable for Timestamp { - fn into_query>(self, name: I) -> WriteQuery { - WriteQuery::new(self, name.into()) + type Error = Infallible; + + fn try_into_query>(self, name: I) -> Result { + Ok(WriteQuery::new(self, name.into())) } } @@ -232,14 +256,17 @@ pub enum QueryType { #[cfg(test)] mod tests { + #[cfg(feature = "chrono")] use super::consts::{ MILLIS_PER_SECOND, MINUTES_PER_HOUR, NANOS_PER_MICRO, NANOS_PER_MILLI, SECONDS_PER_MINUTE, }; use crate::query::{Timestamp, ValidQuery}; + #[test] fn test_equality_str() { assert_eq!(ValidQuery::from("hello"), "hello"); } + #[test] fn test_equality_string() { assert_eq!( @@ -247,15 +274,17 @@ mod tests { String::from("hello") ); } + #[test] fn test_format_for_timestamp_else() { assert!(format!("{}", Timestamp::Nanoseconds(100)) == "100"); } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_hours() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Hours(2).into(); + let datetime_from_timestamp: DateTime = Timestamp::Hours(2).try_into().unwrap(); assert_eq!( Utc.timestamp_nanos( (2 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND * NANOS_PER_MILLI) @@ -265,11 +294,12 @@ mod tests { datetime_from_timestamp ) } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_minutes() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Minutes(2).into(); + let datetime_from_timestamp: DateTime = Timestamp::Minutes(2).try_into().unwrap(); assert_eq!( Utc.timestamp_nanos( (2 * SECONDS_PER_MINUTE * MILLIS_PER_SECOND * NANOS_PER_MILLI) @@ -279,11 +309,12 @@ mod tests { datetime_from_timestamp ) } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_seconds() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Seconds(2).into(); + let datetime_from_timestamp: DateTime = Timestamp::Seconds(2).try_into().unwrap(); assert_eq!( Utc.timestamp_nanos( (2 * MILLIS_PER_SECOND * NANOS_PER_MILLI) @@ -293,33 +324,37 @@ mod tests { datetime_from_timestamp ) } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_millis() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Milliseconds(2).into(); + let datetime_from_timestamp: DateTime = Timestamp::Milliseconds(2).try_into().unwrap(); assert_eq!( Utc.timestamp_nanos((2 * NANOS_PER_MILLI).try_into().unwrap()), datetime_from_timestamp ) } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_nanos() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Nanoseconds(1).into(); + let datetime_from_timestamp: DateTime = Timestamp::Nanoseconds(1).try_into().unwrap(); assert_eq!(Utc.timestamp_nanos(1), datetime_from_timestamp) } + #[cfg(feature = "chrono")] #[test] fn test_chrono_datetime_from_timestamp_micros() { use chrono::prelude::*; - let datetime_from_timestamp: DateTime = Timestamp::Microseconds(2).into(); + let datetime_from_timestamp: DateTime = Timestamp::Microseconds(2).try_into().unwrap(); assert_eq!( Utc.timestamp_nanos((2 * NANOS_PER_MICRO).try_into().unwrap()), datetime_from_timestamp ) } + #[cfg(feature = "chrono")] #[test] fn test_timestamp_from_chrono_date() { @@ -328,7 +363,8 @@ mod tests { .with_ymd_and_hms(1970, 1, 1, 0, 0, 1) .single() .unwrap() - .into(); + .try_into() + .unwrap(); assert_eq!( Timestamp::Nanoseconds(MILLIS_PER_SECOND * NANOS_PER_MILLI), timestamp_from_datetime diff --git a/influxdb/src/query/write_query.rs b/influxdb/src/query/write_query.rs index 148f1072..72ee4770 100644 --- a/influxdb/src/query/write_query.rs +++ b/influxdb/src/query/write_query.rs @@ -55,10 +55,13 @@ impl WriteQuery { /// # Examples /// /// ```rust - /// use influxdb::{Query, Timestamp}; - /// use influxdb::InfluxDbWriteable; + /// use influxdb::{InfluxDbWriteable, Query, Timestamp}; /// - /// Timestamp::Nanoseconds(0).into_query("measurement").add_field("field1", 5).build(); + /// Timestamp::Nanoseconds(0) + /// .try_into_query("measurement") + /// .unwrap() + /// .add_field("field1", 5) + /// .build(); /// ``` #[must_use = "Creating a query is pointless unless you execute it"] pub fn add_field(mut self, field: S, value: F) -> Self @@ -82,7 +85,7 @@ impl WriteQuery { /// use influxdb::InfluxDbWriteable; /// /// Timestamp::Nanoseconds(0) - /// .into_query("measurement") + /// .try_into_query("measurement").unwrap() /// .add_tag("field1", 5); // calling `.build()` now would result in a `Err(Error::InvalidQueryError)` /// ``` #[must_use = "Creating a query is pointless unless you execute it"] @@ -154,23 +157,24 @@ impl From<&str> for Type { } #[cfg(feature = "chrono")] -impl From> for Type { - fn from(dt: chrono::DateTime) -> Self { - match dt.timestamp_nanos_opt() { - Some(nanos) => Type::SignedInteger(nanos), - None => { - // For dates before 1677-09-21, or after - // 2262-04-11, we're just going to return 0. - Type::SignedInteger(0) - } - } +impl TryFrom> for Type { + type Error = crate::error::TimestampTooLargeError; + + fn try_from(dt: chrono::DateTime) -> Result { + let nanos = dt + .timestamp_nanos_opt() + .ok_or(crate::error::TimestampTooLargeError(()))?; + Ok(Self::SignedInteger(nanos)) } } #[cfg(feature = "time")] -impl From for Type { - fn from(dt: time::UtcDateTime) -> Self { - Type::SignedInteger(dt.unix_timestamp_nanos().try_into().unwrap_or(0)) +impl TryFrom for Type { + type Error = >::Error; + + fn try_from(dt: time::UtcDateTime) -> Result { + let nanos = dt.unix_timestamp_nanos().try_into()?; + Ok(Self::SignedInteger(nanos)) } } @@ -296,7 +300,8 @@ mod tests { #[test] fn test_write_builder_empty_query() { let query = Timestamp::Hours(5) - .into_query("marina_3".to_string()) + .try_into_query("marina_3".to_string()) + .unwrap() .build(); assert!(query.is_err(), "Query was not empty"); @@ -305,7 +310,8 @@ mod tests { #[test] fn test_write_builder_single_field() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82) .build(); @@ -316,7 +322,8 @@ mod tests { #[test] fn test_write_builder_multiple_fields() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82) .add_field("wind_strength", 3.7) .add_field("temperature_unsigned", 82u64) @@ -332,7 +339,8 @@ mod tests { #[test] fn test_write_builder_multiple_fields_with_v2() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82) .add_field("wind_strength", 3.7) .add_field("temperature_unsigned", 82u64) @@ -348,7 +356,8 @@ mod tests { #[test] fn test_write_builder_optional_fields() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82u64) .add_tag("wind_strength", >::None) .build(); @@ -360,7 +369,8 @@ mod tests { #[test] fn test_write_builder_optional_fields_with_v2() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82u64) .add_tag("wind_strength", >::None) .build_with_opts(true); @@ -372,7 +382,8 @@ mod tests { #[test] fn test_write_builder_only_tags() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_tag("season", "summer") .build(); @@ -382,7 +393,8 @@ mod tests { #[test] fn test_write_builder_full_query() { let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82) .add_tag("location", "us-midwest") .add_tag("season", "summer") @@ -400,7 +412,8 @@ mod tests { use crate::query::QueryType; let query = Timestamp::Hours(11) - .into_query("weather".to_string()) + .try_into_query("weather".to_string()) + .unwrap() .add_field("temperature", 82) .add_tag("location", "us-midwest") .add_tag("season", "summer"); @@ -411,7 +424,8 @@ mod tests { #[test] fn test_escaping() { let query = Timestamp::Hours(11) - .into_query("wea, ther=") + .try_into_query("wea, ther=") + .unwrap() .add_field("temperature", 82) .add_field("\"temp=era,t ure\"", r#"too"\\hot"#) .add_field("float", 82.0) @@ -430,12 +444,14 @@ mod tests { #[test] fn test_batch() { let q0 = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82) .add_tag("location", "us-midwest"); let q1 = Timestamp::Hours(12) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 65) .add_tag("location", "us-midwest"); diff --git a/influxdb/tests/derive_integration_tests.rs b/influxdb/tests/derive_integration_tests.rs index 751899a6..440e4374 100644 --- a/influxdb/tests/derive_integration_tests.rs +++ b/influxdb/tests/derive_integration_tests.rs @@ -48,13 +48,16 @@ struct WeatherReadingWithoutIgnored { #[test] fn test_build_query() { let weather_reading = WeatherReading { - time: Timestamp::Hours(1).into(), + time: Timestamp::Hours(1).try_into().unwrap(), humidity: 30, pressure: 100, wind_strength: Some(5), }; - let query = weather_reading.into_query("weather_reading"); - let query = query.build().unwrap(); + let query = weather_reading + .try_into_query("weather_reading") + .unwrap() + .build() + .unwrap(); assert_eq!( query.get(), "weather_reading,wind_strength=5 pressure=100i 3600000000000" @@ -64,14 +67,17 @@ fn test_build_query() { #[test] fn test_build_nonstandard_query() { let weather_reading = WeatherReadingWithNonstandardTime { - reading_time: Timestamp::Hours(1).into(), - time: Timestamp::Hours(1).into(), + reading_time: Timestamp::Hours(1).try_into().unwrap(), + time: Timestamp::Hours(1).try_into().unwrap(), humidity: 30, pressure: 100, wind_strength: Some(5), }; - let query = weather_reading.into_query("weather_reading"); - let query = query.build().unwrap(); + let query = weather_reading + .try_into_query("weather_reading") + .unwrap() + .build() + .unwrap(); assert_eq!( query.get(), "weather_reading,wind_strength=5 pressure=100i 3600000000000" @@ -92,12 +98,12 @@ async fn test_derive_simple_write() { create_db(TEST_NAME).await.expect("could not setup db"); let client = create_client(TEST_NAME); let weather_reading = WeatherReading { - time: Timestamp::Nanoseconds(0).into(), + time: Timestamp::Nanoseconds(0).try_into().unwrap(), humidity: 30, wind_strength: Some(5), pressure: 100, }; - let query = weather_reading.into_query("weather_reading"); + let query = weather_reading.try_into_query("weather_reading").unwrap(); let result = client.query(&query).await; assert!(result.is_ok(), "unable to insert into db"); }, @@ -123,13 +129,17 @@ async fn test_write_and_read_option() { 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(), + time: Timestamp::Hours(11).try_into().unwrap(), humidity: 30, wind_strength: None, pressure: 100, }; let write_result = client - .query(&weather_reading.into_query("weather_reading".to_string())) + .query( + &weather_reading + .try_into_query("weather_reading".to_string()) + .unwrap(), + ) .await; assert_result_ok(&write_result); @@ -141,7 +151,7 @@ async fn test_write_and_read_option() { assert_result_ok(&result); let result = result.unwrap(); let value = &result.series[0].values[0]; - assert_eq!(value.time, Timestamp::Hours(11).into()); + assert_eq!(value.time, Timestamp::Hours(11).try_into().unwrap()); assert_eq!(value.pressure, 100); assert_eq!(value.wind_strength, None); }, diff --git a/influxdb/tests/integration_tests.rs b/influxdb/tests/integration_tests.rs index 2261c720..a1432826 100644 --- a/influxdb/tests/integration_tests.rs +++ b/influxdb/tests/integration_tests.rs @@ -8,8 +8,7 @@ use utilities::{ assert_result_err, assert_result_ok, create_client, create_db, delete_db, run_test, }; -use influxdb::InfluxDbWriteable; -use influxdb::{Client, Error, ReadQuery, Timestamp}; +use influxdb::{Client, Error, InfluxDbWriteable, ReadQuery, Timestamp}; /// INTEGRATION TEST /// @@ -70,7 +69,8 @@ async fn test_authed_write_and_read() { let client = Client::new("http://127.0.0.1:9086", TEST_NAME).with_auth("admin", "password"); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(write_query).await; assert_result_ok(&write_result); @@ -120,7 +120,8 @@ async fn test_wrong_authed_write_and_read() { let client = Client::new("http://127.0.0.1:9086", TEST_NAME).with_auth("wrong_user", "password"); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(write_query).await; assert_result_err(&write_result); @@ -190,7 +191,8 @@ async fn test_non_authed_write_and_read() { .expect("could not setup db"); let non_authed_client = Client::new("http://127.0.0.1:9086", TEST_NAME); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = non_authed_client.query(write_query).await; assert_result_err(&write_result); @@ -240,7 +242,8 @@ async fn test_write_and_read_field() { create_db(TEST_NAME).await.expect("could not setup db"); let client = create_client(TEST_NAME); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(write_query).await; assert_result_ok(&write_result); @@ -361,7 +364,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 = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82) .add_field("wind_strength", >::None); let write_result = client.query(write_query).await; @@ -419,7 +423,8 @@ async fn test_json_query() { let client = create_client(TEST_NAME); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(write_query).await; assert_result_ok(&write_result); @@ -469,7 +474,8 @@ async fn test_json_query_tagged() { let client = create_client(TEST_NAME); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_tag("location", "London") .add_field("temperature", 82); let write_result = client.query(write_query).await; @@ -530,13 +536,16 @@ async fn test_json_query_vec() { let client = create_client(TEST_NAME); let write_query1 = Timestamp::Hours(11) - .into_query("temperature_vec") + .try_into_query("temperature_vec") + .unwrap() .add_field("temperature", 16); let write_query2 = Timestamp::Hours(12) - .into_query("temperature_vec") + .try_into_query("temperature_vec") + .unwrap() .add_field("temperature", 17); let write_query3 = Timestamp::Hours(13) - .into_query("temperature_vec") + .try_into_query("temperature_vec") + .unwrap() .add_field("temperature", 18); let _write_result = client.query(write_query1).await; @@ -591,10 +600,12 @@ async fn test_serde_multi_query() { let client = create_client(TEST_NAME); let write_query = Timestamp::Hours(11) - .into_query("temperature") + .try_into_query("temperature") + .unwrap() .add_field("temperature", 16); let write_query2 = Timestamp::Hours(11) - .into_query("humidity") + .try_into_query("humidity") + .unwrap() .add_field("humidity", 69); let write_result = client.query(write_query).await; diff --git a/influxdb/tests/integration_tests_v2.rs b/influxdb/tests/integration_tests_v2.rs index 303a4b6c..48eb21f5 100644 --- a/influxdb/tests/integration_tests_v2.rs +++ b/influxdb/tests/integration_tests_v2.rs @@ -4,8 +4,7 @@ extern crate influxdb; mod utilities; use utilities::{assert_result_err, assert_result_ok, run_test}; -use influxdb::InfluxDbWriteable; -use influxdb::{Client, Error, ReadQuery, Timestamp}; +use influxdb::{Client, Error, InfluxDbWriteable, ReadQuery, Timestamp}; /// INTEGRATION TEST /// @@ -17,7 +16,8 @@ async fn test_authed_write_and_read() { || async move { let client = Client::new("http://127.0.0.1:2086", "mydb").with_token("admintoken"); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(&write_query).await; assert_result_ok(&write_result); @@ -53,7 +53,8 @@ async fn test_wrong_authed_write_and_read() { || async move { let client = Client::new("http://127.0.0.1:2086", "mydb").with_token("falsetoken"); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = client.query(&write_query).await; assert_result_err(&write_result); @@ -93,7 +94,8 @@ async fn test_non_authed_write_and_read() { || async move { let non_authed_client = Client::new("http://127.0.0.1:2086", "mydb"); let write_query = Timestamp::Hours(11) - .into_query("weather") + .try_into_query("weather") + .unwrap() .add_field("temperature", 82); let write_result = non_authed_client.query(&write_query).await; assert_result_err(&write_result); diff --git a/influxdb_derive/Cargo.toml b/influxdb_derive/Cargo.toml index 093ae634..8a30d53b 100644 --- a/influxdb_derive/Cargo.toml +++ b/influxdb_derive/Cargo.toml @@ -23,3 +23,4 @@ proc-macro = true proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["extra-traits", "full"] } +syn-path = "2.1" diff --git a/influxdb_derive/src/writeable.rs b/influxdb_derive/src/writeable.rs index d66018a6..fa3c9bd6 100644 --- a/influxdb_derive/src/writeable.rs +++ b/influxdb_derive/src/writeable.rs @@ -1,14 +1,18 @@ -use proc_macro2::TokenStream; -use quote::quote; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Data, DeriveInput, Field, Fields, Ident, Meta, Token, + AngleBracketedGenericArguments, Data, DeriveInput, Field, Fields, GenericArgument, Ident, + Lifetime, Meta, PathArguments, PredicateType, Token, Type, TypeParamBound, WhereClause, + WherePredicate, }; +use syn_path::type_path; #[derive(Debug)] struct WriteableField { ident: Ident, + ty: Type, is_time: bool, is_tag: bool, is_ignore: bool, @@ -57,6 +61,7 @@ impl TryFrom for WriteableField { fn try_from(field: Field) -> syn::Result { let ident = field.ident.expect("fields without ident are not supported"); + let ty = field.ty; let mut has_time_attr = false; let mut is_tag = false; let mut is_ignore = false; @@ -92,6 +97,7 @@ impl TryFrom for WriteableField { Ok(WriteableField { ident, + ty, is_time, is_tag, is_ignore, @@ -130,17 +136,20 @@ pub fn expand_writeable(input: DeriveInput) -> syn::Result { // Find the time field let mut time_field = None; + let mut time_field_ty = None; for wf in &writeable_fields { if wf.is_time { if time_field.is_some() { panic!("multiple time fields found!"); } time_field = Some(wf.ident.clone()); + time_field_ty = Some(wf.ty.clone()); } } // There must be exactly one time field let time_field = time_field.expect("no time field found"); + let time_field_ty = time_field_ty.unwrap(); // Generate field assignments (excluding time and ignored fields) let field_assignments = writeable_fields @@ -158,16 +167,134 @@ pub fn expand_writeable(input: DeriveInput) -> syn::Result { }) .collect::>(); + // Add a necessary where clause + let mut where_clause = where_clause.cloned().unwrap_or(WhereClause { + where_token: Default::default(), + predicates: Punctuated::new(), + }); + let mut err_ty = type_path!(<::influxdb::Timestamp as ::core::convert::TryFrom>::Error); + err_ty + .path + .segments + .iter_mut() + .nth(err_ty.qself.as_ref().unwrap().position - 1) + .unwrap() + .arguments = PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: [GenericArgument::Type(time_field_ty.clone())] + .into_iter() + .collect(), + gt_token: Default::default(), + }); + where_clause + .predicates + .push(WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: Type::Path(err_ty), + colon_token: Default::default(), + bounds: [TypeParamBound::Lifetime(Lifetime { + apostrophe: Span::call_site(), + ident: format_ident!("static"), + })] + .into_iter() + .collect(), + })); + + // Assemble the rest of the code Ok(quote! { - impl #impl_generics ::influxdb::InfluxDbWriteable for #ident #ty_generics #where_clause { - fn into_query>(self, name: I) -> ::influxdb::WriteQuery { - let timestamp: ::influxdb::Timestamp = self.#time_field.into(); - let mut query = timestamp.into_query(name); - #( - query = #field_assignments; - )* - query + const _: () = { + mod __influxdb_private { + use ::influxdb::{InfluxDbWriteable, Timestamp}; + use ::core::fmt::{self, Debug, Display, Formatter, Write as _}; + + pub enum Error + where + Timestamp: TryFrom + { + TimestampError(>::Error), + QueryError(::Error) + } + + impl Clone for Error + where + Timestamp: TryFrom, + >::Error: Clone + { + fn clone(&self) -> Self { + match self { + Self::TimestampError(err) => Self::TimestampError(err.clone()), + Self::QueryError(err) => Self::QueryError(err.clone()) + } + } + } + + impl Debug for Error + where + Timestamp: TryFrom, + >::Error: Debug + { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::TimestampError(err) => f.debug_tuple("TimestampError") + .field(err) + .finish(), + Self::QueryError(err) => f.debug_tuple("QueryError") + .field(err) + .finish() + } + } + } + + impl Display for Error + where + Timestamp: TryFrom, + >::Error: Display + { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::TimestampError(err) => { + write!(f, "Unable to convert value to timestamp: {err}") + }, + Self::QueryError(err) => { + write!(f, "Unable to convert timestamp to query: {err}") + } + } + } + } + + impl ::std::error::Error for Error + where + Timestamp: TryFrom, + >::Error: ::std::error::Error + 'static + { + fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { + match self { + Self::TimestampError(err) => Some(err), + Self::QueryError(err) => Some(err) + } + } + } } - } + + impl #impl_generics ::influxdb::InfluxDbWriteable for #ident #ty_generics #where_clause { + type Error = __influxdb_private::Error<#time_field_ty>; + + fn try_into_query>( + self, + name: I + ) -> ::core::result::Result<::influxdb::WriteQuery, Self::Error> { + let timestamp: ::influxdb::Timestamp = self.#time_field + .try_into() + .map_err(__influxdb_private::Error::TimestampError)?; + let mut query = timestamp.try_into_query(name) + .map_err(__influxdb_private::Error::QueryError)?; + #( + query = #field_assignments; + )* + Ok(query) + } + } + }; }) }