diff --git a/Cargo.toml b/Cargo.toml index 9e45eaa..385c9da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ serde_plain = { version = "1.0.2", optional = true } serde_repr = "0.1.19" socket2 = "0.5.7" thiserror = "1.0.63" -time = { version = "0.3.36", features = ["formatting", "parsing"], optional = true } +time = { version = "0.3.36", features = ["formatting", "parsing", "macros"], optional = true } tokio = { workspace = true, features = ["net", "rt", "io-util"] } tracing = { workspace = true } tracing-futures = { version = "0.2.5", features = ["futures-03"], optional = true } diff --git a/src/api/autogen/index.ts b/src/api/autogen/index.ts index bcf1f46..60c7ed7 100644 --- a/src/api/autogen/index.ts +++ b/src/api/autogen/index.ts @@ -311,12 +311,11 @@ function handleType( case 'string': { let { format } = schema; if (format === 'date-time' || format === 'date-time-fits') { - format = format === 'date-time' ? 'DATE_TIME_OFFSET' : 'DATE_TIME'; - format = `time::format_description::well_known::Iso8601::${format}`; + format = format === 'date-time' ? 'Iso8601' : 'Fits'; let viaType = baseKind === 'Request' ? 'TimeParam' : 'TimeResponse'; return rusty( 'std::time::SystemTime', - `time_repr::${viaType}<{ time_repr::config_from(${format}) }>` + `time_repr::${viaType}` ); } return rusty('String'); diff --git a/src/api/mod.rs b/src/api/mod.rs index 67c7851..8b63ec4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -910,7 +910,7 @@ pub trait Camera: Device + Send + Sync { } /// Reports the actual exposure start in the FITS-standard CCYY-MM-DDThh:mm:ss[.sss...] format. - #[http("lastexposurestarttime", method = Get, via = time_repr::TimeResponse<{ time_repr::config_from(time::format_description::well_known::Iso8601::DATE_TIME) }>)] + #[http("lastexposurestarttime", method = Get, via = time_repr::TimeResponse)] async fn last_exposure_start_time(&self) -> ASCOMResult { Err(ASCOMError::NOT_IMPLEMENTED) } @@ -2297,7 +2297,7 @@ pub trait Telescope: Device + Send + Sync { } /// Returns the UTC date/time of the telescope's internal clock. - #[http("utcdate", method = Get, via = time_repr::TimeResponse<{ time_repr::config_from(time::format_description::well_known::Iso8601::DATE_TIME_OFFSET) }>)] + #[http("utcdate", method = Get, via = time_repr::TimeResponse)] async fn utc_date(&self) -> ASCOMResult { Err(ASCOMError::NOT_IMPLEMENTED) } @@ -2307,7 +2307,7 @@ pub trait Telescope: Device + Send + Sync { async fn set_utc_date( &self, - #[http("UTCDate", via = time_repr::TimeParam<{ time_repr::config_from(time::format_description::well_known::Iso8601::DATE_TIME_OFFSET) }>)] + #[http("UTCDate", via = time_repr::TimeParam)] utc_date: std::time::SystemTime, ) -> ASCOMResult { Err(ASCOMError::NOT_IMPLEMENTED) diff --git a/src/api/time_repr.rs b/src/api/time_repr.rs index 2495e66..09df92b 100644 --- a/src/api/time_repr.rs +++ b/src/api/time_repr.rs @@ -1,78 +1,96 @@ use crate::response::ValueResponse; use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; use std::time::SystemTime; -use time::format_description::well_known::iso8601::EncodedConfig; -use time::format_description::well_known::Iso8601; +use time::formatting::Formattable; +use time::macros::format_description; +use time::parsing::Parsable; +use time::{format_description, OffsetDateTime}; -pub(crate) struct TimeParam(SystemTime); +pub(crate) trait FormatWrapper { + type Format: 'static + ?Sized + Parsable + Formattable; -// `time` crate doesn't expose config from `Iso8601`, so we have to extract one manually via inference -pub(crate) const fn config_from(_: Iso8601) -> EncodedConfig { - CONFIG + const FORMAT: &'static Self::Format; } -impl std::fmt::Debug for TimeParam { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } +pub(crate) struct Iso8601; + +impl FormatWrapper for Iso8601 { + type Format = format_description::well_known::Iso8601; + + const FORMAT: &'static Self::Format = &Self::Format::DEFAULT; +} + +pub(crate) struct Fits; + +impl FormatWrapper for Fits { + type Format = [format_description::BorrowedFormatItem<'static>]; + + const FORMAT: &'static Self::Format = format_description!( + "[year]-[month]-[day]T[hour]:[minute]:[second][optional [.[subsecond digits:3]]]" + ); } -impl From for TimeParam { +#[derive(Debug)] +pub(crate) struct TimeParam(OffsetDateTime, PhantomData); + +impl From for TimeParam { fn from(value: SystemTime) -> Self { - Self(value) + Self(value.into(), PhantomData) } } -impl From> for SystemTime { - fn from(wrapper: TimeParam) -> Self { - wrapper.0 +impl From> for SystemTime { + fn from(wrapper: TimeParam) -> Self { + wrapper.0.into() } } -impl Serialize for TimeParam { +impl Serialize for TimeParam { fn serialize(&self, serializer: S) -> Result { - time::OffsetDateTime::from(self.0) - .format(&Iso8601::) + self.0 + .format(&F::FORMAT) .map_err(serde::ser::Error::custom)? .serialize(serializer) } } -impl<'de, const CONFIG: EncodedConfig> Deserialize<'de> for TimeParam { +impl<'de, F: FormatWrapper> Deserialize<'de> for TimeParam { fn deserialize>(deserializer: D) -> Result { - struct Visitor; + struct Visitor(PhantomData); - impl serde::de::Visitor<'_> for Visitor { - type Value = TimeParam<{ CONFIG }>; + impl serde::de::Visitor<'_> for Visitor { + type Value = TimeParam; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a date string") } fn visit_str(self, value: &str) -> Result { - match time::OffsetDateTime::parse(value, &Iso8601::) { - Ok(value) => Ok(TimeParam(value.into())), + match time::PrimitiveDateTime::parse(value, &F::FORMAT) { + Ok(value) => Ok(TimeParam(value.assume_utc(), PhantomData)), Err(err) => Err(serde::de::Error::custom(err)), } } } - deserializer.deserialize_str(Visitor) + deserializer.deserialize_str(Visitor(PhantomData)) } } #[derive(Debug, Serialize, Deserialize)] +#[serde(bound = "F: FormatWrapper")] #[serde(transparent)] -pub(crate) struct TimeResponse(ValueResponse>); +pub(crate) struct TimeResponse(ValueResponse>); -impl From for TimeResponse { +impl From for TimeResponse { fn from(value: SystemTime) -> Self { Self(TimeParam::from(value).into()) } } -impl From> for SystemTime { - fn from(wrapper: TimeResponse) -> Self { +impl From> for SystemTime { + fn from(wrapper: TimeResponse) -> Self { wrapper.0.into().into() } }