use std::fmt;
use chrono::{DateTime, Utc};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const DATETIME_MIN_EPOCH_MILLIS: i64 = -62_135_596_800_000;
const DATE_1900_EPOCH_MILLIS: i64 = -2_208_988_800_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AspNetDate(pub Option<DateTime<Utc>>);
impl AspNetDate {
pub fn value(&self) -> Option<&DateTime<Utc>> {
self.0.as_ref()
}
}
impl<'de> Deserialize<'de> for AspNetDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(AspNetDateVisitor)
}
}
struct AspNetDateVisitor;
impl<'de> Visitor<'de> for AspNetDateVisitor {
type Value = AspNetDate;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a string in /Date(epoch_ms)/ format, empty string, or null")
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(AspNetDate(None))
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
if value.is_empty() {
return Ok(AspNetDate(None));
}
let inner = value
.strip_prefix("/Date(")
.and_then(|s| s.strip_suffix(")/"))
.ok_or_else(|| de::Error::custom(format!("invalid ASP.NET date format: {value:?}")))?;
let epoch_millis: i64 = inner.parse().map_err(|_| {
de::Error::custom(format!(
"invalid epoch milliseconds in ASP.NET date: {inner:?}"
))
})?;
if epoch_millis == DATETIME_MIN_EPOCH_MILLIS || epoch_millis == DATE_1900_EPOCH_MILLIS {
return Ok(AspNetDate(None));
}
let dt = DateTime::from_timestamp_millis(epoch_millis)
.ok_or_else(|| de::Error::custom(format!("out-of-range timestamp: {epoch_millis}")))?;
Ok(AspNetDate(Some(dt)))
}
}
impl Serialize for AspNetDate {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match &self.0 {
Some(dt) => serializer.serialize_str(&dt.to_rfc3339()),
None => serializer.serialize_none(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlexBool(pub Option<bool>);
impl FlexBool {
pub fn value(&self) -> Option<bool> {
self.0
}
}
impl<'de> Deserialize<'de> for FlexBool {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(FlexBoolVisitor)
}
}
struct FlexBoolVisitor;
impl<'de> Visitor<'de> for FlexBoolVisitor {
type Value = FlexBool;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a boolean, 0, 1, or null")
}
fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
Ok(FlexBool(Some(value)))
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
match value {
0 => Ok(FlexBool(Some(false))),
1 => Ok(FlexBool(Some(true))),
other => Err(de::Error::custom(format!("expected 0 or 1, got {other}"))),
}
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
match value {
0 => Ok(FlexBool(Some(false))),
1 => Ok(FlexBool(Some(true))),
other => Err(de::Error::custom(format!("expected 0 or 1, got {other}"))),
}
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(FlexBool(None))
}
}
impl Serialize for FlexBool {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self.0 {
Some(b) => serializer.serialize_bool(b),
None => serializer.serialize_none(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aspnet_date_valid() {
let d: AspNetDate = serde_json::from_str(r#""/Date(1767225600000)/""#).unwrap();
let expected = DateTime::from_timestamp_millis(1_767_225_600_000).unwrap();
assert_eq!(d, AspNetDate(Some(expected)));
}
#[test]
fn aspnet_date_null() {
let d: AspNetDate = serde_json::from_str("null").unwrap();
assert_eq!(d, AspNetDate(None));
}
#[test]
fn aspnet_date_empty_string() {
let d: AspNetDate = serde_json::from_str(r#""""#).unwrap();
assert_eq!(d, AspNetDate(None));
}
#[test]
fn aspnet_date_dotnet_min() {
let d: AspNetDate = serde_json::from_str(r#""/Date(-62135596800000)/""#).unwrap();
assert_eq!(d, AspNetDate(None));
}
#[test]
fn aspnet_date_1900_sentinel() {
let d: AspNetDate = serde_json::from_str(r#""/Date(-2208988800000)/""#).unwrap();
assert_eq!(d, AspNetDate(None));
}
#[test]
fn aspnet_date_invalid_format() {
let result: Result<AspNetDate, _> = serde_json::from_str(r#""2026-01-01""#);
assert!(result.is_err());
}
#[test]
fn aspnet_date_invalid_type() {
let result: Result<AspNetDate, _> = serde_json::from_str("42");
assert!(result.is_err());
}
#[test]
fn aspnet_date_serialize_some() {
let dt = DateTime::from_timestamp(1_767_225_600, 0).unwrap();
let d = AspNetDate(Some(dt));
let json = serde_json::to_string(&d).unwrap();
assert_eq!(json, format!(r#""{}""#, dt.to_rfc3339()));
}
#[test]
fn aspnet_date_serialize_none() {
let json = serde_json::to_string(&AspNetDate(None)).unwrap();
assert_eq!(json, "null");
}
#[test]
fn flex_bool_true() {
let b: FlexBool = serde_json::from_str("true").unwrap();
assert_eq!(b, FlexBool(Some(true)));
}
#[test]
fn flex_bool_false() {
let b: FlexBool = serde_json::from_str("false").unwrap();
assert_eq!(b, FlexBool(Some(false)));
}
#[test]
fn flex_bool_one() {
let b: FlexBool = serde_json::from_str("1").unwrap();
assert_eq!(b, FlexBool(Some(true)));
}
#[test]
fn flex_bool_zero() {
let b: FlexBool = serde_json::from_str("0").unwrap();
assert_eq!(b, FlexBool(Some(false)));
}
#[test]
fn flex_bool_null() {
let b: FlexBool = serde_json::from_str("null").unwrap();
assert_eq!(b, FlexBool(None));
}
#[test]
fn flex_bool_invalid_string() {
let result: Result<FlexBool, _> = serde_json::from_str(r#""true""#);
assert!(result.is_err());
}
#[test]
fn flex_bool_invalid_number() {
let result: Result<FlexBool, _> = serde_json::from_str("2");
assert!(result.is_err());
}
#[test]
fn flex_bool_serialize_true() {
let json = serde_json::to_string(&FlexBool(Some(true))).unwrap();
assert_eq!(json, "true");
}
#[test]
fn flex_bool_serialize_false() {
let json = serde_json::to_string(&FlexBool(Some(false))).unwrap();
assert_eq!(json, "false");
}
#[test]
fn flex_bool_serialize_none() {
let json = serde_json::to_string(&FlexBool(None)).unwrap();
assert_eq!(json, "null");
}
}