1use std::fmt;
2use std::str::FromStr;
3
4use chrono::{Datelike, NaiveDate, NaiveTime, TimeDelta, TimeZone, Utc, Weekday};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::error::Error;
8
9pub type DateTime = chrono::DateTime<Utc>;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub struct Date(pub NaiveDate);
18
19impl Date {
20 pub fn new(year: i32, month: u32, day: u32) -> Option<Date> {
22 NaiveDate::from_ymd_opt(year, month, day).map(Date)
23 }
24
25 pub fn parse(source: &str) -> Result<Date, Error> {
27 source
28 .parse()
29 .map_err(|error| Error::usage(format!("invalid date {source:?}: {error}")))
30 }
31
32 pub fn today() -> Date {
34 Date(chrono::Local::now().date_naive())
35 }
36
37 pub fn today_utc() -> Date {
39 Date(Utc::now().date_naive())
40 }
41
42 pub fn from_datetime(moment: &DateTime) -> Date {
44 Date(moment.date_naive())
45 }
46
47 pub fn from_datetime_in<Tz: TimeZone>(moment: &DateTime, zone: &Tz) -> Date {
50 Date(moment.with_timezone(zone).date_naive())
51 }
52
53 pub fn year(&self) -> i32 {
55 self.0.year()
56 }
57
58 pub fn month(&self) -> u32 {
60 self.0.month()
61 }
62
63 pub fn day(&self) -> u32 {
65 self.0.day()
66 }
67
68 pub fn weekday(&self) -> Weekday {
70 self.0.weekday()
71 }
72
73 pub fn at_midnight_utc(&self) -> DateTime {
80 self.0.and_time(NaiveTime::MIN).and_utc()
81 }
82
83 pub fn add_days(&self, days: i64) -> Option<Date> {
86 self.0
87 .checked_add_signed(TimeDelta::try_days(days)?)
88 .map(Date)
89 }
90
91 pub fn add_months(&self, months: i32) -> Option<Date> {
95 let target = i64::from(self.0.year()) * 12 + i64::from(self.0.month0()) + i64::from(months);
96 let year = i32::try_from(target.div_euclid(12)).ok()?;
97 let month = u32::try_from(target.rem_euclid(12)).ok()? + 1;
98 Date::new(year, month, 1)?.add_days(i64::from(self.0.day() - 1))
99 }
100
101 pub fn add_years(&self, years: i32) -> Option<Date> {
104 self.add_months(years.checked_mul(12)?)
105 }
106
107 pub fn days_since(&self, earlier: Date) -> i64 {
109 self.0.signed_duration_since(earlier.0).num_days()
110 }
111}
112
113impl From<NaiveDate> for Date {
114 fn from(date: NaiveDate) -> Date {
115 Date(date)
116 }
117}
118
119impl From<Date> for NaiveDate {
120 fn from(date: Date) -> NaiveDate {
121 date.0
122 }
123}
124
125impl fmt::Display for Date {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "{}", self.0.format("%Y-%m-%d"))
128 }
129}
130
131impl FromStr for Date {
132 type Err = chrono::ParseError;
133
134 fn from_str(source: &str) -> Result<Date, chrono::ParseError> {
135 NaiveDate::parse_from_str(source, "%Y-%m-%d").map(Date)
136 }
137}
138
139impl Serialize for Date {
140 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
141 serializer.serialize_str(&self.to_string())
142 }
143}
144
145impl<'de> Deserialize<'de> for Date {
146 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Date, D::Error> {
147 let text = String::deserialize(deserializer)?;
148 text.parse().map_err(serde::de::Error::custom)
149 }
150}
151
152pub mod optional_date {
156 use serde::{Deserialize, Deserializer, Serialize, Serializer};
157
158 use super::Date;
159
160 pub fn serialize<S: Serializer>(date: &Option<Date>, serializer: S) -> Result<S::Ok, S::Error> {
162 date.serialize(serializer)
163 }
164
165 pub fn deserialize<'de, D: Deserializer<'de>>(
167 deserializer: D,
168 ) -> Result<Option<Date>, D::Error> {
169 match Option::<String>::deserialize(deserializer)? {
170 Some(text) if !text.is_empty() => {
171 text.parse().map(Some).map_err(serde::de::Error::custom)
172 }
173 _ => Ok(None),
174 }
175 }
176}
177
178pub mod null_as_default {
187 use serde::{Deserialize, Deserializer};
188
189 pub fn deserialize<'de, D: Deserializer<'de>, T: Deserialize<'de> + Default>(
191 deserializer: D,
192 ) -> Result<T, D::Error> {
193 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
194 }
195}
196
197#[derive(Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
200#[serde(transparent)]
201pub struct SensitiveString(String);
202
203impl SensitiveString {
204 pub fn new(value: impl Into<String>) -> SensitiveString {
206 SensitiveString(value.into())
207 }
208
209 pub fn expose(&self) -> &str {
211 &self.0
212 }
213
214 pub fn into_inner(self) -> String {
216 self.0
217 }
218
219 pub fn is_empty(&self) -> bool {
221 self.0.is_empty()
222 }
223}
224
225impl From<String> for SensitiveString {
226 fn from(value: String) -> SensitiveString {
227 SensitiveString(value)
228 }
229}
230
231impl From<&str> for SensitiveString {
232 fn from(value: &str) -> SensitiveString {
233 SensitiveString(value.to_string())
234 }
235}
236
237impl fmt::Debug for SensitiveString {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 if self.0.is_empty() {
240 f.write_str("\"\"")
241 } else {
242 f.write_str("[REDACTED]")
243 }
244 }
245}
246
247impl fmt::Display for SensitiveString {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 if self.0.is_empty() {
250 Ok(())
251 } else {
252 f.write_str("[REDACTED]")
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn dates_round_trip_through_json() {
263 let date: Date = serde_json::from_str("\"2026-03-04\"").unwrap();
264 assert_eq!(date, Date::new(2026, 3, 4).unwrap());
265 assert_eq!(serde_json::to_string(&date).unwrap(), "\"2026-03-04\"");
266 }
267
268 #[test]
269 fn sensitive_strings_hide_their_value() {
270 let secret = SensitiveString::new("jane@example.com");
271 assert_eq!(format!("{secret:?}"), "[REDACTED]");
272 assert_eq!(secret.to_string(), "[REDACTED]");
273 assert_eq!(secret.expose(), "jane@example.com");
274 assert_eq!(
275 serde_json::to_string(&secret).unwrap(),
276 "\"jane@example.com\""
277 );
278 }
279}