use async_graphql::{Error, Value as GqlValue};
use chrono::format::{Item, StrftimeItems};
use dynamic_graphql::{ResolvedObject, ResolvedObjectFields, Scalar, ScalarValue};
use raphtory_api::core::{
storage::timeindex::{AsTime, EventTime},
utils::time::{IntoTime, TryIntoTime},
};
#[derive(Scalar, Clone, Debug)]
#[graphql(name = "TimeInput")]
pub struct GqlTimeInput(pub EventTime);
impl ScalarValue for GqlTimeInput {
fn from_value(value: GqlValue) -> Result<Self, Error> {
match value {
GqlValue::Number(timestamp) => timestamp
.as_i64()
.ok_or(Error::new(
"Expected Int, DateTime formatted String, or Object { timestamp, eventId }.",
))
.map(|timestamp| GqlTimeInput(EventTime::start(timestamp))),
GqlValue::String(dt) => dt
.try_into_time()
.map(|t| GqlTimeInput(t.set_event_id(0)))
.map_err(|e| Error::new(e.to_string())),
GqlValue::Object(obj) => {
let timestamp_val = obj
.get("timestamp")
.or_else(|| obj.get("time")) .ok_or_else(|| Error::new("Object must contain 'timestamp' (or 'time')."))?;
let ts = match timestamp_val {
GqlValue::Number(n) => n
.as_i64()
.ok_or(Error::new("timestamp must be an Int or a DateTime String."))?,
GqlValue::String(s) => s
.try_into_time()
.map_err(|e| Error::new(e.to_string()))?
.t(),
_ => return Err(Error::new("timestamp must be an Int or a DateTime String.")),
};
let idx_val = obj
.get("eventId")
.or_else(|| obj.get("id")) .ok_or_else(|| Error::new("Object must contain 'eventId' (or 'id')."))?;
let idx: usize = match idx_val {
GqlValue::Number(n) => {
let u = n
.as_u64()
.ok_or(Error::new("eventId must be a non-negative Int."))?;
usize::try_from(u).map_err(|_| Error::new("index out of range"))?
}
_ => return Err(Error::new("eventId must be a non-negative Int.")),
};
Ok(GqlTimeInput(EventTime::new(ts, idx)))
}
_ => Err(Error::new(
"Expected Int, DateTime formatted String, or Object { timestamp, eventId }.",
)),
}
}
fn to_value(&self) -> GqlValue {
self.0.t().into()
}
}
impl From<i64> for GqlTimeInput {
fn from(value: i64) -> Self {
GqlTimeInput(EventTime::start(value))
}
}
impl IntoTime for GqlTimeInput {
fn into_time(self) -> EventTime {
self.0
}
}
pub fn dt_format_str_is_valid(fmt_str: &str) -> bool {
if StrftimeItems::new(fmt_str).any(|it| matches!(it, Item::Error)) {
false
} else {
true
}
}
#[derive(ResolvedObject, Clone, Copy)]
#[graphql(name = "EventTime")]
pub struct GqlEventTime {
pub(crate) inner: Option<EventTime>,
}
#[ResolvedObjectFields]
impl GqlEventTime {
async fn timestamp(&self) -> Option<i64> {
self.inner.map(|t| t.t())
}
async fn event_id(&self) -> Option<u64> {
self.inner.map(|t| t.i() as u64)
}
async fn datetime(&self, format_string: Option<String>) -> Result<Option<String>, Error> {
let fmt_string = format_string.as_deref().unwrap_or("%+"); if dt_format_str_is_valid(fmt_string) {
self.inner
.map(|t| {
t.dt()
.map(|dt| dt.format(fmt_string).to_string())
.map_err(|e| Error::new(e.to_string()))
})
.transpose()
} else {
Err(Error::new(format!(
"Invalid datetime format string: '{}'",
fmt_string
)))
}
}
}
impl From<Option<EventTime>> for GqlEventTime {
fn from(value: Option<EventTime>) -> Self {
Self { inner: value }
}
}
impl From<EventTime> for GqlEventTime {
fn from(value: EventTime) -> Self {
Self { inner: Some(value) }
}
}
impl From<GqlEventTime> for Option<EventTime> {
fn from(value: GqlEventTime) -> Self {
value.inner
}
}