use std::fmt;
use std::str::FromStr;
use chrono::{Datelike, NaiveDate, NaiveTime, TimeDelta, TimeZone, Utc, Weekday};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::Error;
pub type DateTime = chrono::DateTime<Utc>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Date(pub NaiveDate);
impl Date {
pub fn new(year: i32, month: u32, day: u32) -> Option<Date> {
NaiveDate::from_ymd_opt(year, month, day).map(Date)
}
pub fn parse(source: &str) -> Result<Date, Error> {
source
.parse()
.map_err(|error| Error::usage(format!("invalid date {source:?}: {error}")))
}
pub fn today() -> Date {
Date(chrono::Local::now().date_naive())
}
pub fn today_utc() -> Date {
Date(Utc::now().date_naive())
}
pub fn from_datetime(moment: &DateTime) -> Date {
Date(moment.date_naive())
}
pub fn from_datetime_in<Tz: TimeZone>(moment: &DateTime, zone: &Tz) -> Date {
Date(moment.with_timezone(zone).date_naive())
}
pub fn year(&self) -> i32 {
self.0.year()
}
pub fn month(&self) -> u32 {
self.0.month()
}
pub fn day(&self) -> u32 {
self.0.day()
}
pub fn weekday(&self) -> Weekday {
self.0.weekday()
}
pub fn at_midnight_utc(&self) -> DateTime {
self.0.and_time(NaiveTime::MIN).and_utc()
}
pub fn add_days(&self, days: i64) -> Option<Date> {
self.0
.checked_add_signed(TimeDelta::try_days(days)?)
.map(Date)
}
pub fn add_months(&self, months: i32) -> Option<Date> {
let target = i64::from(self.0.year()) * 12 + i64::from(self.0.month0()) + i64::from(months);
let year = i32::try_from(target.div_euclid(12)).ok()?;
let month = target.rem_euclid(12) as u32 + 1;
Date::new(year, month, 1)?.add_days(i64::from(self.0.day() - 1))
}
pub fn add_years(&self, years: i32) -> Option<Date> {
self.add_months(years.checked_mul(12)?)
}
pub fn days_since(&self, earlier: Date) -> i64 {
self.0.signed_duration_since(earlier.0).num_days()
}
}
impl From<NaiveDate> for Date {
fn from(date: NaiveDate) -> Date {
Date(date)
}
}
impl From<Date> for NaiveDate {
fn from(date: Date) -> NaiveDate {
date.0
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.format("%Y-%m-%d"))
}
}
impl FromStr for Date {
type Err = chrono::ParseError;
fn from_str(source: &str) -> Result<Date, chrono::ParseError> {
NaiveDate::parse_from_str(source, "%Y-%m-%d").map(Date)
}
}
impl Serialize for Date {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Date, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
pub mod optional_date {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::Date;
pub fn serialize<S: Serializer>(date: &Option<Date>, serializer: S) -> Result<S::Ok, S::Error> {
date.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Date>, D::Error> {
match Option::<String>::deserialize(deserializer)? {
Some(text) if !text.is_empty() => {
text.parse().map(Some).map_err(serde::de::Error::custom)
}
_ => Ok(None),
}
}
}
pub mod null_as_default {
use serde::{Deserialize, Deserializer};
pub fn deserialize<'de, D: Deserializer<'de>, T: Deserialize<'de> + Default>(
deserializer: D,
) -> Result<T, D::Error> {
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
}
#[derive(Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SensitiveString(String);
impl SensitiveString {
pub fn new(value: impl Into<String>) -> SensitiveString {
SensitiveString(value.into())
}
pub fn expose(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<String> for SensitiveString {
fn from(value: String) -> SensitiveString {
SensitiveString(value)
}
}
impl From<&str> for SensitiveString {
fn from(value: &str) -> SensitiveString {
SensitiveString(value.to_string())
}
}
impl fmt::Debug for SensitiveString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
f.write_str("\"\"")
} else {
f.write_str("[REDACTED]")
}
}
}
impl fmt::Display for SensitiveString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
Ok(())
} else {
f.write_str("[REDACTED]")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dates_round_trip_through_json() {
let date: Date = serde_json::from_str("\"2026-03-04\"").unwrap();
assert_eq!(date, Date::new(2026, 3, 4).unwrap());
assert_eq!(serde_json::to_string(&date).unwrap(), "\"2026-03-04\"");
}
#[test]
fn sensitive_strings_hide_their_value() {
let secret = SensitiveString::new("jane@example.com");
assert_eq!(format!("{secret:?}"), "[REDACTED]");
assert_eq!(secret.to_string(), "[REDACTED]");
assert_eq!(secret.expose(), "jane@example.com");
assert_eq!(
serde_json::to_string(&secret).unwrap(),
"\"jane@example.com\""
);
}
}