From 694ec7ea98945b503cba49bd2ad58a3de4601789 Mon Sep 17 00:00:00 2001 From: Dominik Werder Date: Wed, 23 Nov 2022 10:38:07 +0100 Subject: [PATCH] Factor out date serde into newtype --- daqbufp2/src/test/api1.rs | 4 +- err/src/lib.rs | 7 +++ netpod/src/query.rs | 1 + netpod/src/query/api1.rs | 114 ++++++++--------------------------- netpod/src/query/datetime.rs | 86 ++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 93 deletions(-) create mode 100644 netpod/src/query/datetime.rs diff --git a/daqbufp2/src/test/api1.rs b/daqbufp2/src/test/api1.rs index 7e2fbab..c7c1da6 100644 --- a/daqbufp2/src/test/api1.rs +++ b/daqbufp2/src/test/api1.rs @@ -25,9 +25,7 @@ fn events_f64_plain() -> Result<(), Error> { let node = &cluster.nodes[0]; let url: Url = format!("http://{}:{}/api/1/query", node.host, node.port).parse()?; let accept = "application/octet-stream"; - let beg: DateTime = "1970-01-01T00:00:00Z".parse()?; - let end: DateTime = "1970-01-01T00:01:00Z".parse()?; - let range = Api1Range::new(beg, end)?; + let range = Api1Range::new("1970-01-01T00:00:00Z".try_into()?, "1970-01-01T00:01:00Z".try_into()?)?; // TODO the channel list needs to get pre-processed to check for backend prefix! let ch = ChannelTuple::new("test-disk-databuffer".into(), "scalar-i32-be".into()); let qu = Api1Query::new(range, vec![ch]); diff --git a/err/src/lib.rs b/err/src/lib.rs index bb8648a..ab27d66 100644 --- a/err/src/lib.rs +++ b/err/src/lib.rs @@ -4,6 +4,7 @@ Error handling and reporting. use serde::{Deserialize, Serialize}; use std::array::TryFromSliceError; +use std::convert::Infallible; use std::fmt; use std::net::AddrParseError; use std::num::{ParseFloatError, ParseIntError}; @@ -239,6 +240,12 @@ impl From for Error { } } +impl ToErr for Infallible { + fn to_err(self) -> Error { + Error::with_msg_no_trace(String::new()) + } +} + impl From for Error { fn from(k: String) -> Self { Self::with_msg(k) diff --git a/netpod/src/query.rs b/netpod/src/query.rs index 03c8d2b..0e74578 100644 --- a/netpod/src/query.rs +++ b/netpod/src/query.rs @@ -1,4 +1,5 @@ pub mod api1; +pub mod datetime; use crate::get_url_query_pairs; use crate::{log::*, DiskIoTune}; diff --git a/netpod/src/query/api1.rs b/netpod/src/query/api1.rs index 537f4bc..76fa6c0 100644 --- a/netpod/src/query/api1.rs +++ b/netpod/src/query/api1.rs @@ -1,5 +1,5 @@ +use crate::query::datetime::Datetime; use crate::{DiskIoTune, FileIoBufferSize, ReadSys}; -use chrono::{DateTime, FixedOffset}; use err::Error; use serde::{Deserialize, Serialize}; use std::fmt; @@ -16,96 +16,27 @@ fn bool_is_true(x: &bool) -> bool { pub struct Api1Range { #[serde(rename = "type", default, skip_serializing_if = "String::is_empty")] ty: String, - #[serde( - rename = "startDate", - serialize_with = "datetime_serde::ser", - deserialize_with = "datetime_serde::de" - )] - beg: DateTime, - #[serde( - rename = "endDate", - serialize_with = "datetime_serde::ser", - deserialize_with = "datetime_serde::de" - )] - end: DateTime, -} - -mod datetime_serde { - use super::*; - use serde::de::Visitor; - use serde::{Deserializer, Serializer}; - - // RFC 3339 / ISO 8601 - - pub fn ser(val: &DateTime, ser: S) -> Result { - use fmt::Write; - let mut s = String::with_capacity(64); - write!(&mut s, "{}", val.format("%Y-%m-%dT%H:%M:%S")).map_err(|_| serde::ser::Error::custom("fmt"))?; - let mus = val.timestamp_subsec_micros(); - if mus % 1000 != 0 { - write!(&mut s, "{}", val.format(".%6f")).map_err(|_| serde::ser::Error::custom("fmt"))?; - } else if mus != 0 { - write!(&mut s, "{}", val.format(".%3f")).map_err(|_| serde::ser::Error::custom("fmt"))?; - } - if val.offset().local_minus_utc() == 0 { - write!(&mut s, "Z").map_err(|_| serde::ser::Error::custom("fmt"))?; - } else { - write!(&mut s, "{}", val.format("%:z")).map_err(|_| serde::ser::Error::custom("fmt"))?; - } - ser.serialize_str(&s) - } - - struct DateTimeVisitor {} - - impl<'de> Visitor<'de> for DateTimeVisitor { - type Value = DateTime; - - fn expecting(&self, fmt: &mut fmt::Formatter) -> std::fmt::Result { - write!(fmt, "DateTimeWithOffset") - } - - fn visit_str(self, val: &str) -> Result - where - E: serde::de::Error, - { - let res = DateTime::::parse_from_rfc3339(val); - match res { - Ok(res) => Ok(res), - // TODO deliver better fine grained error - Err(e) => Err(serde::de::Error::custom(format!("{e}"))), - } - } - } - - pub fn de<'de, D>(de: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - de.deserialize_str(DateTimeVisitor {}) - } + #[serde(rename = "startDate")] + beg: Datetime, + #[serde(rename = "endDate")] + end: Datetime, } impl Api1Range { - pub fn new(beg: A, end: B) -> Result - where - A: TryInto>, - B: TryInto>, - >>::Error: fmt::Debug, - >>::Error: fmt::Debug, - { + pub fn new(beg: Datetime, end: Datetime) -> Result { let ret = Self { ty: String::new(), - beg: beg.try_into().map_err(|e| format!("{e:?}"))?, - end: end.try_into().map_err(|e| format!("{e:?}"))?, + beg, + end, }; Ok(ret) } - pub fn beg(&self) -> &DateTime { + pub fn beg(&self) -> &Datetime { &self.beg } - pub fn end(&self) -> &DateTime { + pub fn end(&self) -> &Datetime { &self.end } } @@ -132,7 +63,7 @@ fn serde_de_range_offset() { #[test] fn serde_ser_range_offset() { - use chrono::{NaiveDate, TimeZone}; + use chrono::{FixedOffset, NaiveDate, TimeZone}; let beg = FixedOffset::east_opt(60 * 60 * 3) .unwrap() .from_local_datetime( @@ -153,30 +84,32 @@ fn serde_ser_range_offset() { ) .earliest() .unwrap(); - let range = Api1Range::new(beg, end).unwrap(); + let range = Api1Range::new(beg.into(), end.into()).unwrap(); let js = serde_json::to_string(&range).unwrap(); let exp = r#"{"startDate":"2022-11-22T13:14:15.016+03:00","endDate":"2022-11-22T13:14:15.800-01:00"}"#; assert_eq!(js, exp); } #[test] -fn serde_ser_range_01() { - let beg: DateTime = "2022-11-22T02:03:04Z".parse().unwrap(); - let end: DateTime = "2022-11-22T02:03:04.123Z".parse().unwrap(); +fn serde_ser_range_01() -> Result<(), Error> { + let beg = Datetime::try_from("2022-11-22T02:03:04Z")?; + let end = Datetime::try_from("2022-11-22T02:03:04.123Z")?; let range = Api1Range::new(beg, end).unwrap(); let js = serde_json::to_string(&range).unwrap(); let exp = r#"{"startDate":"2022-11-22T02:03:04Z","endDate":"2022-11-22T02:03:04.123Z"}"#; assert_eq!(js, exp); + Ok(()) } #[test] -fn serde_ser_range_02() { - let beg: DateTime = "2022-11-22T02:03:04.987654Z".parse().unwrap(); - let end: DateTime = "2022-11-22T02:03:04.777000Z".parse().unwrap(); +fn serde_ser_range_02() -> Result<(), Error> { + let beg = Datetime::try_from("2022-11-22T02:03:04.987654Z")?; + let end = Datetime::try_from("2022-11-22T02:03:04.777000Z")?; let range = Api1Range::new(beg, end).unwrap(); let js = serde_json::to_string(&range).unwrap(); let exp = r#"{"startDate":"2022-11-22T02:03:04.987654Z","endDate":"2022-11-22T02:03:04.777Z"}"#; assert_eq!(js, exp); + Ok(()) } /// In Api1, the list of channels consists of either `BACKEND/CHANNELNAME` @@ -356,9 +289,9 @@ impl Api1Query { } #[test] -fn serde_api1_query() { - let beg: DateTime = "2022-11-22T08:09:10Z".parse().unwrap(); - let end: DateTime = "2022-11-23T08:11:05.455009+02:00".parse().unwrap(); +fn serde_api1_query() -> Result<(), Error> { + let beg = Datetime::try_from("2022-11-22T08:09:10Z")?; + let end = Datetime::try_from("2022-11-23T08:11:05.455009+02:00")?; let range = Api1Range::new(beg, end).unwrap(); let ch0 = ChannelTuple::from_name("nameonly".into()); let ch1 = ChannelTuple::new("somebackend".into(), "somechan".into()); @@ -368,4 +301,5 @@ fn serde_api1_query() { js, r#"{"range":{"startDate":"2022-11-22T08:09:10Z","endDate":"2022-11-23T08:11:05.455009+02:00"},"channels":["nameonly","somebackend/somechan"]}"# ); + Ok(()) } diff --git a/netpod/src/query/datetime.rs b/netpod/src/query/datetime.rs new file mode 100644 index 0000000..446ce50 --- /dev/null +++ b/netpod/src/query/datetime.rs @@ -0,0 +1,86 @@ +use chrono::{DateTime, FixedOffset}; +use err::Error; +use serde::{de::Visitor, Deserialize, Serialize}; +use std::fmt; +use std::ops; + +#[derive(Clone, Debug, PartialEq)] +pub struct Datetime(DateTime); + +impl Datetime {} + +impl From> for Datetime { + fn from(x: DateTime) -> Self { + Datetime(x) + } +} + +impl TryFrom<&str> for Datetime { + type Error = Error; + + fn try_from(val: &str) -> Result { + let dt = + DateTime::::parse_from_rfc3339(val).map_err(|e| Error::with_msg_no_trace(format!("{e}")))?; + Ok(Datetime(dt)) + } +} + +impl ops::Deref for Datetime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// RFC 3339 (subset of ISO 8601) + +impl Serialize for Datetime { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use fmt::Write; + let val = &self.0; + let mut s = String::with_capacity(64); + write!(&mut s, "{}", val.format("%Y-%m-%dT%H:%M:%S")).map_err(|_| serde::ser::Error::custom("fmt"))?; + let mus = val.timestamp_subsec_micros(); + if mus % 1000 != 0 { + write!(&mut s, "{}", val.format(".%6f")).map_err(|_| serde::ser::Error::custom("fmt"))?; + } else if mus != 0 { + write!(&mut s, "{}", val.format(".%3f")).map_err(|_| serde::ser::Error::custom("fmt"))?; + } + if val.offset().local_minus_utc() == 0 { + write!(&mut s, "Z").map_err(|_| serde::ser::Error::custom("fmt"))?; + } else { + write!(&mut s, "{}", val.format("%:z")).map_err(|_| serde::ser::Error::custom("fmt"))?; + } + serializer.collect_str(&s) + } +} + +struct Vis1; + +impl<'de> Visitor<'de> for Vis1 { + type Value = Datetime; + + fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "Datetime") + } + + fn visit_str(self, val: &str) -> Result + where + E: serde::de::Error, + { + Datetime::try_from(val).map_err(|e| serde::de::Error::custom(format!("{e}"))) + } +} + +impl<'de> Deserialize<'de> for Datetime { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Vis1) + } +}