use std::cmp::max;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use chrono::FixedOffset;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClockTimezoneOffset(FixedOffset);
impl ClockTimezoneOffset {
pub fn offset_ms(&self) -> i64 {
i64::from(self.0.local_minus_utc()) * 1_000
}
}
impl FromStr for ClockTimezoneOffset {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
FixedOffset::from_str(s).map(Self).map_err(|e| {
format!(
"invalid timezone offset {s:?} (expected a UTC offset such as \"+05:30\" or \"-08:00\"): {e}"
)
})
}
}
impl Display for ClockTimezoneOffset {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for ClockTimezoneOffset {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ClockTimezoneOffset {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
}
fn is_zero(value: &i64) -> bool {
*value == 0
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct ClockConfig {
pub clock_resolution_usecs: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub timezone_offset_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub now_offset_ms: Option<i64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub http_driven: bool,
}
impl ClockConfig {
pub fn clock_resolution_ms(&self) -> u64 {
max((self.clock_resolution_usecs + 500) / 1_000, 1)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct ClockAdvanceRequest {
#[serde(default)]
pub delta_ms: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct ClockAdvanceResponse {
pub now_ms: i64,
pub now: String,
}
#[cfg(test)]
mod test {
use super::ClockTimezoneOffset;
#[test]
fn timezone_offset_parses_and_round_trips() {
const MINUTE_MS: i64 = 60_000;
for (input, expected_ms) in [
("+05:30", (5 * 60 + 30) * MINUTE_MS),
("-08:00", -8 * 60 * MINUTE_MS),
("+00:00", 0),
("+14:00", 14 * 60 * MINUTE_MS),
] {
let offset: ClockTimezoneOffset =
serde_json::from_value(serde_json::json!(input)).unwrap();
assert_eq!(offset.offset_ms(), expected_ms, "input {input}");
assert_eq!(
serde_json::to_value(offset).unwrap(),
serde_json::json!(input),
"round trip of {input}"
);
}
}
#[test]
fn clock_config_without_offset_defaults_to_zero() {
let config: super::ClockConfig =
serde_json::from_value(serde_json::json!({"clock_resolution_usecs": 1_000_000}))
.unwrap();
assert_eq!(config.timezone_offset_ms, 0);
let runtime_config: crate::config::RuntimeConfig =
serde_json::from_value(serde_json::json!({"workers": 4})).unwrap();
assert_eq!(runtime_config.clock_timezone_offset, None);
}
#[test]
fn timezone_offset_rejects_invalid_input() {
for input in [
serde_json::json!("05:30"),
serde_json::json!("banana"),
serde_json::json!("+27:00"),
serde_json::json!(330),
] {
assert!(
serde_json::from_value::<ClockTimezoneOffset>(input.clone()).is_err(),
"input {input} should be rejected"
);
}
}
}