Factor out date serde into newtype
This commit is contained in:
@@ -25,9 +25,7 @@ fn events_f64_plain() -> Result<(), Error> {
|
|||||||
let node = &cluster.nodes[0];
|
let node = &cluster.nodes[0];
|
||||||
let url: Url = format!("http://{}:{}/api/1/query", node.host, node.port).parse()?;
|
let url: Url = format!("http://{}:{}/api/1/query", node.host, node.port).parse()?;
|
||||||
let accept = "application/octet-stream";
|
let accept = "application/octet-stream";
|
||||||
let beg: DateTime<Utc> = "1970-01-01T00:00:00Z".parse()?;
|
let range = Api1Range::new("1970-01-01T00:00:00Z".try_into()?, "1970-01-01T00:01:00Z".try_into()?)?;
|
||||||
let end: DateTime<Utc> = "1970-01-01T00:01:00Z".parse()?;
|
|
||||||
let range = Api1Range::new(beg, end)?;
|
|
||||||
// TODO the channel list needs to get pre-processed to check for backend prefix!
|
// 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 ch = ChannelTuple::new("test-disk-databuffer".into(), "scalar-i32-be".into());
|
||||||
let qu = Api1Query::new(range, vec![ch]);
|
let qu = Api1Query::new(range, vec![ch]);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Error handling and reporting.
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::array::TryFromSliceError;
|
use std::array::TryFromSliceError;
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::net::AddrParseError;
|
use std::net::AddrParseError;
|
||||||
use std::num::{ParseFloatError, ParseIntError};
|
use std::num::{ParseFloatError, ParseIntError};
|
||||||
@@ -239,6 +240,12 @@ impl From<PublicError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToErr for Infallible {
|
||||||
|
fn to_err(self) -> Error {
|
||||||
|
Error::with_msg_no_trace(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<String> for Error {
|
impl From<String> for Error {
|
||||||
fn from(k: String) -> Self {
|
fn from(k: String) -> Self {
|
||||||
Self::with_msg(k)
|
Self::with_msg(k)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod api1;
|
pub mod api1;
|
||||||
|
pub mod datetime;
|
||||||
|
|
||||||
use crate::get_url_query_pairs;
|
use crate::get_url_query_pairs;
|
||||||
use crate::{log::*, DiskIoTune};
|
use crate::{log::*, DiskIoTune};
|
||||||
|
|||||||
+24
-90
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::query::datetime::Datetime;
|
||||||
use crate::{DiskIoTune, FileIoBufferSize, ReadSys};
|
use crate::{DiskIoTune, FileIoBufferSize, ReadSys};
|
||||||
use chrono::{DateTime, FixedOffset};
|
|
||||||
use err::Error;
|
use err::Error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -16,96 +16,27 @@ fn bool_is_true(x: &bool) -> bool {
|
|||||||
pub struct Api1Range {
|
pub struct Api1Range {
|
||||||
#[serde(rename = "type", default, skip_serializing_if = "String::is_empty")]
|
#[serde(rename = "type", default, skip_serializing_if = "String::is_empty")]
|
||||||
ty: String,
|
ty: String,
|
||||||
#[serde(
|
#[serde(rename = "startDate")]
|
||||||
rename = "startDate",
|
beg: Datetime,
|
||||||
serialize_with = "datetime_serde::ser",
|
#[serde(rename = "endDate")]
|
||||||
deserialize_with = "datetime_serde::de"
|
end: Datetime,
|
||||||
)]
|
|
||||||
beg: DateTime<FixedOffset>,
|
|
||||||
#[serde(
|
|
||||||
rename = "endDate",
|
|
||||||
serialize_with = "datetime_serde::ser",
|
|
||||||
deserialize_with = "datetime_serde::de"
|
|
||||||
)]
|
|
||||||
end: DateTime<FixedOffset>,
|
|
||||||
}
|
|
||||||
|
|
||||||
mod datetime_serde {
|
|
||||||
use super::*;
|
|
||||||
use serde::de::Visitor;
|
|
||||||
use serde::{Deserializer, Serializer};
|
|
||||||
|
|
||||||
// RFC 3339 / ISO 8601
|
|
||||||
|
|
||||||
pub fn ser<S: Serializer>(val: &DateTime<FixedOffset>, ser: S) -> Result<S::Ok, S::Error> {
|
|
||||||
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<FixedOffset>;
|
|
||||||
|
|
||||||
fn expecting(&self, fmt: &mut fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(fmt, "DateTimeWithOffset")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
let res = DateTime::<FixedOffset>::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<DateTime<FixedOffset>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
de.deserialize_str(DateTimeVisitor {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Api1Range {
|
impl Api1Range {
|
||||||
pub fn new<A, B>(beg: A, end: B) -> Result<Self, Error>
|
pub fn new(beg: Datetime, end: Datetime) -> Result<Self, Error> {
|
||||||
where
|
|
||||||
A: TryInto<DateTime<FixedOffset>>,
|
|
||||||
B: TryInto<DateTime<FixedOffset>>,
|
|
||||||
<A as TryInto<DateTime<FixedOffset>>>::Error: fmt::Debug,
|
|
||||||
<B as TryInto<DateTime<FixedOffset>>>::Error: fmt::Debug,
|
|
||||||
{
|
|
||||||
let ret = Self {
|
let ret = Self {
|
||||||
ty: String::new(),
|
ty: String::new(),
|
||||||
beg: beg.try_into().map_err(|e| format!("{e:?}"))?,
|
beg,
|
||||||
end: end.try_into().map_err(|e| format!("{e:?}"))?,
|
end,
|
||||||
};
|
};
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beg(&self) -> &DateTime<FixedOffset> {
|
pub fn beg(&self) -> &Datetime {
|
||||||
&self.beg
|
&self.beg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end(&self) -> &DateTime<FixedOffset> {
|
pub fn end(&self) -> &Datetime {
|
||||||
&self.end
|
&self.end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +63,7 @@ fn serde_de_range_offset() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_ser_range_offset() {
|
fn serde_ser_range_offset() {
|
||||||
use chrono::{NaiveDate, TimeZone};
|
use chrono::{FixedOffset, NaiveDate, TimeZone};
|
||||||
let beg = FixedOffset::east_opt(60 * 60 * 3)
|
let beg = FixedOffset::east_opt(60 * 60 * 3)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.from_local_datetime(
|
.from_local_datetime(
|
||||||
@@ -153,30 +84,32 @@ fn serde_ser_range_offset() {
|
|||||||
)
|
)
|
||||||
.earliest()
|
.earliest()
|
||||||
.unwrap();
|
.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 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"}"#;
|
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);
|
assert_eq!(js, exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_ser_range_01() {
|
fn serde_ser_range_01() -> Result<(), Error> {
|
||||||
let beg: DateTime<FixedOffset> = "2022-11-22T02:03:04Z".parse().unwrap();
|
let beg = Datetime::try_from("2022-11-22T02:03:04Z")?;
|
||||||
let end: DateTime<FixedOffset> = "2022-11-22T02:03:04.123Z".parse().unwrap();
|
let end = Datetime::try_from("2022-11-22T02:03:04.123Z")?;
|
||||||
let range = Api1Range::new(beg, end).unwrap();
|
let range = Api1Range::new(beg, end).unwrap();
|
||||||
let js = serde_json::to_string(&range).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"}"#;
|
let exp = r#"{"startDate":"2022-11-22T02:03:04Z","endDate":"2022-11-22T02:03:04.123Z"}"#;
|
||||||
assert_eq!(js, exp);
|
assert_eq!(js, exp);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_ser_range_02() {
|
fn serde_ser_range_02() -> Result<(), Error> {
|
||||||
let beg: DateTime<FixedOffset> = "2022-11-22T02:03:04.987654Z".parse().unwrap();
|
let beg = Datetime::try_from("2022-11-22T02:03:04.987654Z")?;
|
||||||
let end: DateTime<FixedOffset> = "2022-11-22T02:03:04.777000Z".parse().unwrap();
|
let end = Datetime::try_from("2022-11-22T02:03:04.777000Z")?;
|
||||||
let range = Api1Range::new(beg, end).unwrap();
|
let range = Api1Range::new(beg, end).unwrap();
|
||||||
let js = serde_json::to_string(&range).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"}"#;
|
let exp = r#"{"startDate":"2022-11-22T02:03:04.987654Z","endDate":"2022-11-22T02:03:04.777Z"}"#;
|
||||||
assert_eq!(js, exp);
|
assert_eq!(js, exp);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In Api1, the list of channels consists of either `BACKEND/CHANNELNAME`
|
/// In Api1, the list of channels consists of either `BACKEND/CHANNELNAME`
|
||||||
@@ -356,9 +289,9 @@ impl Api1Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_api1_query() {
|
fn serde_api1_query() -> Result<(), Error> {
|
||||||
let beg: DateTime<FixedOffset> = "2022-11-22T08:09:10Z".parse().unwrap();
|
let beg = Datetime::try_from("2022-11-22T08:09:10Z")?;
|
||||||
let end: DateTime<FixedOffset> = "2022-11-23T08:11:05.455009+02:00".parse().unwrap();
|
let end = Datetime::try_from("2022-11-23T08:11:05.455009+02:00")?;
|
||||||
let range = Api1Range::new(beg, end).unwrap();
|
let range = Api1Range::new(beg, end).unwrap();
|
||||||
let ch0 = ChannelTuple::from_name("nameonly".into());
|
let ch0 = ChannelTuple::from_name("nameonly".into());
|
||||||
let ch1 = ChannelTuple::new("somebackend".into(), "somechan".into());
|
let ch1 = ChannelTuple::new("somebackend".into(), "somechan".into());
|
||||||
@@ -368,4 +301,5 @@ fn serde_api1_query() {
|
|||||||
js,
|
js,
|
||||||
r#"{"range":{"startDate":"2022-11-22T08:09:10Z","endDate":"2022-11-23T08:11:05.455009+02:00"},"channels":["nameonly","somebackend/somechan"]}"#
|
r#"{"range":{"startDate":"2022-11-22T08:09:10Z","endDate":"2022-11-23T08:11:05.455009+02:00"},"channels":["nameonly","somebackend/somechan"]}"#
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<FixedOffset>);
|
||||||
|
|
||||||
|
impl Datetime {}
|
||||||
|
|
||||||
|
impl From<DateTime<FixedOffset>> for Datetime {
|
||||||
|
fn from(x: DateTime<FixedOffset>) -> Self {
|
||||||
|
Datetime(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Datetime {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(val: &str) -> Result<Self, Self::Error> {
|
||||||
|
let dt =
|
||||||
|
DateTime::<FixedOffset>::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<FixedOffset>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 3339 (subset of ISO 8601)
|
||||||
|
|
||||||
|
impl Serialize for Datetime {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<E>(self, val: &str) -> Result<Self::Value, E>
|
||||||
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(Vis1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user