Skip to main content

satay_runtime/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::fmt;
4use std::str::FromStr;
5
6use http::header::{self, CONTENT_TYPE, HeaderName, HeaderValue};
7#[cfg(feature = "json")]
8use serde::de;
9use time::Month;
10use time::format_description::well_known::Rfc3339;
11pub use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
12
13use tracing::{debug, instrument};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct RequestParts<B> {
17    pub method: http::Method,
18    pub uri: String,
19    pub headers: http::HeaderMap,
20    pub body: B,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ResponseParts<B> {
25    pub status: http::StatusCode,
26    pub headers: http::HeaderMap,
27    pub body: B,
28}
29
30#[derive(Debug, thiserror::Error)]
31pub enum Error {
32    #[error("failed to build HTTP message: {0}")]
33    Http(#[from] http::Error),
34
35    #[error("invalid HTTP header value: {0}")]
36    InvalidHeaderValue(#[from] header::InvalidHeaderValue),
37
38    #[error("invalid HTTP header name: {0}")]
39    InvalidHeaderName(#[from] header::InvalidHeaderName),
40
41    #[error("missing required field `{0}`")]
42    MissingRequired(&'static str),
43
44    #[error("{0}")]
45    InvalidResponse(&'static str),
46
47    #[cfg(feature = "json")]
48    #[error("JSON error: {0}")]
49    Json(#[from] serde_json::Error),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
53pub enum ParseRangeError {
54    #[error("range contains more than one `-` separator")]
55    TooManySeparators,
56
57    #[error("invalid range minimum `{value}`: {message}")]
58    InvalidMinimum { value: String, message: String },
59
60    #[error("invalid range maximum `{value}`: {message}")]
61    InvalidMaximum { value: String, message: String },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
65pub enum ParseTimeError {
66    #[error("time must be exactly four ASCII digits in HHMM format")]
67    InvalidFormat,
68
69    #[error("time is outside valid HHMM range")]
70    ComponentRange,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
74pub enum ParseDateError {
75    #[error("date must be in YYYY-MM-DD format")]
76    InvalidFormat,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
80pub enum ParseNaiveDateTimeError {
81    #[error("datetime must be in YYYY-MM-DDTHH:mm:ss format")]
82    InvalidFormat,
83}
84
85pub trait Action {
86    type Response;
87
88    /// Builds the HTTP request for this action.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if request construction or required input validation fails.
93    fn request(self) -> Result<http::Request<Vec<u8>>, Error>;
94
95    /// Decodes the HTTP response body into this action's response type.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the response is invalid or cannot be decoded.
100    fn decode<B: AsRef<[u8]>>(response: ResponseParts<B>) -> Result<Self::Response, Error>;
101}
102
103/// Converts request parts into an HTTP request.
104///
105/// # Errors
106///
107/// Returns an error if the method, URI, or body cannot be converted into an HTTP request.
108#[instrument(skip_all, fields(method = %method, uri = %uri))]
109pub fn into_request<B>(
110    RequestParts {
111        method,
112        uri,
113        headers,
114        body,
115    }: RequestParts<B>,
116) -> Result<http::Request<B>, Error> {
117    debug!("building HTTP request");
118    let mut request = http::Request::builder()
119        .method(method)
120        .uri(uri)
121        .body(body)?;
122    *request.headers_mut() = headers;
123    Ok(request)
124}
125
126/// Converts request parts with an empty body into an HTTP request.
127///
128/// # Errors
129///
130/// Returns an error if the method or URI cannot be converted into an HTTP request.
131#[instrument(skip_all, fields(method = %method, uri = %uri))]
132pub fn into_empty_request(
133    RequestParts {
134        method,
135        uri,
136        headers,
137        body: (),
138    }: RequestParts<()>,
139) -> Result<http::Request<Vec<u8>>, Error> {
140    debug!("building empty HTTP request");
141    let mut request = http::Request::builder()
142        .method(method)
143        .uri(uri)
144        .body(vec![])?;
145    *request.headers_mut() = headers;
146    Ok(request)
147}
148
149/// Converts serializable request parts into a JSON HTTP request.
150///
151/// # Errors
152///
153/// Returns an error if JSON serialization fails or the HTTP request cannot be built.
154#[cfg(feature = "json")]
155#[instrument(skip_all, fields(method = %method, uri = %uri))]
156pub fn into_json_request<T>(
157    RequestParts {
158        method,
159        uri,
160        headers,
161        body,
162    }: RequestParts<T>,
163) -> Result<http::Request<Vec<u8>>, Error>
164where
165    T: serde::Serialize,
166{
167    debug!("building JSON HTTP request");
168    let body = serde_json::to_vec(&body)?;
169    let mut request = http::Request::builder()
170        .method(method)
171        .uri(uri)
172        .body(body)?;
173    *request.headers_mut() = headers;
174    if !request.headers().contains_key(CONTENT_TYPE) {
175        request.headers_mut().insert(
176            CONTENT_TYPE,
177            http::HeaderValue::from_static("application/json"),
178        );
179    }
180    Ok(request)
181}
182
183/// Converts optional serializable request parts into a JSON HTTP request.
184///
185/// # Errors
186///
187/// Returns an error if JSON serialization fails or the HTTP request cannot be built.
188#[cfg(feature = "json")]
189#[instrument(skip_all, fields(method = %method, uri = %uri))]
190pub fn into_optional_json_request<T>(
191    RequestParts {
192        method,
193        uri,
194        headers,
195        body,
196    }: RequestParts<Option<T>>,
197) -> Result<http::Request<Vec<u8>>, Error>
198where
199    T: serde::Serialize,
200{
201    match body {
202        Some(body) => into_json_request(RequestParts {
203            method,
204            uri,
205            headers,
206            body,
207        }),
208        None => into_empty_request(RequestParts {
209            method,
210            uri,
211            headers,
212            body: (),
213        }),
214    }
215}
216
217/// Deserializes a JSON response body from bytes.
218///
219/// # Errors
220///
221/// Returns an error if the body is not valid JSON for `T`.
222#[cfg(feature = "json")]
223#[instrument(skip_all)]
224pub fn from_json_slice<T>(body: &[u8]) -> Result<T, Error>
225where
226    T: de::DeserializeOwned,
227{
228    debug!("deserializing JSON response");
229    Ok(serde_json::from_slice(body)?)
230}
231
232pub fn append_path_segment(out: &mut String, value: &str) {
233    append_percent_encoded(out, value.as_bytes());
234}
235
236pub fn append_query_pair(out: &mut String, first: &mut bool, key: &str, value: &str) {
237    if *first {
238        out.push('?');
239        *first = false;
240    } else {
241        out.push('&');
242    }
243    append_percent_encoded(out, key.as_bytes());
244    out.push('=');
245    append_percent_encoded(out, value.as_bytes());
246}
247
248#[must_use]
249pub fn format_offset_datetime(value: &OffsetDateTime) -> String {
250    value.format(&Rfc3339).unwrap_or_else(|_| value.to_string())
251}
252
253#[must_use]
254pub fn format_unix_time(value: &OffsetDateTime) -> String {
255    value.unix_timestamp().to_string()
256}
257
258#[must_use]
259pub fn format_date(value: &Date) -> String {
260    format!(
261        "{:04}-{:02}-{:02}",
262        value.year(),
263        u8::from(value.month()),
264        value.day()
265    )
266}
267
268/// Parses a date in `YYYY-MM-DD` format.
269///
270/// # Errors
271///
272/// Returns an error if the value is not in `YYYY-MM-DD` format or is not a valid calendar date.
273pub fn parse_date(value: &str) -> Result<Date, ParseDateError> {
274    let value = value.trim().as_bytes();
275    if value.len() != 10 || value[4] != b'-' || value[7] != b'-' {
276        return Err(ParseDateError::InvalidFormat);
277    }
278
279    for (index, byte) in value.iter().enumerate() {
280        if matches!(index, 4 | 7) {
281            if *byte != b'-' {
282                return Err(ParseDateError::InvalidFormat);
283            }
284        } else if !byte.is_ascii_digit() {
285            return Err(ParseDateError::InvalidFormat);
286        }
287    }
288
289    let year = parse_date_year(&value[0..4])?;
290    let month = parse_date_u8(&value[5..7])?;
291    let day = parse_date_u8(&value[8..10])?;
292    let month = Month::try_from(month).map_err(|_| ParseDateError::InvalidFormat)?;
293    Date::from_calendar_date(year, month, day).map_err(|_| ParseDateError::InvalidFormat)
294}
295
296fn parse_date_year(bytes: &[u8]) -> Result<i32, ParseDateError> {
297    let mut value = 0i32;
298    for byte in bytes {
299        value = value
300            .checked_mul(10)
301            .and_then(|value| value.checked_add(i32::from(*byte - b'0')))
302            .ok_or(ParseDateError::InvalidFormat)?;
303    }
304    Ok(value)
305}
306
307fn parse_date_u8(bytes: &[u8]) -> Result<u8, ParseDateError> {
308    let mut value = 0u16;
309    for byte in bytes {
310        value = value
311            .checked_mul(10)
312            .and_then(|value| value.checked_add(u16::from(*byte - b'0')))
313            .ok_or(ParseDateError::InvalidFormat)?;
314    }
315    u8::try_from(value).map_err(|_| ParseDateError::InvalidFormat)
316}
317
318#[must_use]
319pub fn format_naive_datetime(value: &PrimitiveDateTime) -> String {
320    format!(
321        "{}T{:02}:{:02}:{:02}",
322        format_date(&value.date()),
323        value.hour(),
324        value.minute(),
325        value.second()
326    )
327}
328
329/// Parses a datetime in `YYYY-MM-DDTHH:mm:ss` format.
330///
331/// # Errors
332///
333/// Returns an error if the value is not in the expected format or has invalid date/time fields.
334pub fn parse_naive_datetime(value: &str) -> Result<PrimitiveDateTime, ParseNaiveDateTimeError> {
335    let value = value.trim();
336    let bytes = value.as_bytes();
337    if bytes.len() != 19
338        || bytes[4] != b'-'
339        || bytes[7] != b'-'
340        || bytes[10] != b'T'
341        || bytes[13] != b':'
342        || bytes[16] != b':'
343    {
344        return Err(ParseNaiveDateTimeError::InvalidFormat);
345    }
346
347    for (index, byte) in bytes.iter().enumerate() {
348        if matches!(index, 4 | 7 | 10 | 13 | 16) {
349            continue;
350        }
351        if !byte.is_ascii_digit() {
352            return Err(ParseNaiveDateTimeError::InvalidFormat);
353        }
354    }
355
356    let date = parse_date(&value[0..10]).map_err(|_| ParseNaiveDateTimeError::InvalidFormat)?;
357    let hour = parse_date_u8(&bytes[11..13]).map_err(|_| ParseNaiveDateTimeError::InvalidFormat)?;
358    let minute =
359        parse_date_u8(&bytes[14..16]).map_err(|_| ParseNaiveDateTimeError::InvalidFormat)?;
360    let second =
361        parse_date_u8(&bytes[17..19]).map_err(|_| ParseNaiveDateTimeError::InvalidFormat)?;
362    let time =
363        Time::from_hms(hour, minute, second).map_err(|_| ParseNaiveDateTimeError::InvalidFormat)?;
364    Ok(PrimitiveDateTime::new(date, time))
365}
366
367/// Parses a time in `HHMM` format.
368///
369/// # Errors
370///
371/// Returns an error if the value is not four ASCII digits or is outside the valid time range.
372pub fn parse_time(value: &str) -> Result<Time, ParseTimeError> {
373    let value = value.trim();
374    let bytes = value.as_bytes();
375    if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
376        return Err(ParseTimeError::InvalidFormat);
377    }
378
379    let hour = (bytes[0] - b'0') * 10 + (bytes[1] - b'0');
380    let minute = (bytes[2] - b'0') * 10 + (bytes[3] - b'0');
381    Time::from_hms(hour, minute, 0).map_err(|_| ParseTimeError::ComponentRange)
382}
383
384#[must_use]
385pub fn format_time(value: &Time) -> String {
386    format!("{:02}{:02}", value.hour(), value.minute())
387}
388
389#[must_use]
390pub fn format_bool(value: &bool) -> &'static str {
391    if *value { "1" } else { "0" }
392}
393
394/// Parses an inclusive range string into optional minimum and maximum bounds.
395///
396/// # Errors
397///
398/// Returns an error if the range has too many separators or either bound cannot be parsed as `T`.
399pub fn parse_range<T>(value: &str) -> Result<(Option<T>, Option<T>), ParseRangeError>
400where
401    T: FromStr,
402    T::Err: fmt::Display,
403{
404    let value = value.trim();
405    if value.is_empty() {
406        return Ok((None, None));
407    }
408
409    let (min, max) = match value.split_once('-') {
410        Some((min, max)) => {
411            if max.contains('-') {
412                return Err(ParseRangeError::TooManySeparators);
413            }
414            (min, max)
415        }
416        None => (value, value),
417    };
418
419    Ok((parse_range_min(min)?, parse_range_max(max)?))
420}
421
422#[must_use]
423pub fn format_range<T>(min: &Option<T>, max: &Option<T>) -> String
424where
425    T: fmt::Display,
426{
427    match (min, max) {
428        (Some(min), Some(max)) => format!("{min}-{max}"),
429        (Some(min), None) => format!("{min}-"),
430        (None, Some(max)) => format!("-{max}"),
431        (None, None) => String::new(),
432    }
433}
434
435fn parse_range_min<T>(value: &str) -> Result<Option<T>, ParseRangeError>
436where
437    T: FromStr,
438    T::Err: fmt::Display,
439{
440    parse_range_bound(value, |value, message| ParseRangeError::InvalidMinimum {
441        value,
442        message,
443    })
444}
445
446fn parse_range_max<T>(value: &str) -> Result<Option<T>, ParseRangeError>
447where
448    T: FromStr,
449    T::Err: fmt::Display,
450{
451    parse_range_bound(value, |value, message| ParseRangeError::InvalidMaximum {
452        value,
453        message,
454    })
455}
456
457fn parse_range_bound<T>(
458    value: &str,
459    invalid: impl FnOnce(String, String) -> ParseRangeError,
460) -> Result<Option<T>, ParseRangeError>
461where
462    T: FromStr,
463    T::Err: fmt::Display,
464{
465    let value = value.trim();
466    if value.is_empty() {
467        return Ok(None);
468    }
469
470    value
471        .parse::<T>()
472        .map(Some)
473        .map_err(|err| invalid(value.to_owned(), err.to_string()))
474}
475
476#[cfg(feature = "serde")]
477#[allow(clippy::missing_errors_doc)]
478pub mod serde_string {
479    use std::fmt;
480    use std::str::FromStr;
481
482    use serde::Deserialize;
483    use serde::de::Error as DeError;
484    macro_rules! string_from_str_module {
485        ($module:ident, $ty:ty) => {
486            pub mod $module {
487                pub fn serialize<S>(value: &$ty, serializer: S) -> Result<S::Ok, S::Error>
488                where
489                    S: serde::Serializer,
490                {
491                    super::serialize_display(value, serializer)
492                }
493
494                pub fn deserialize<'de, D>(deserializer: D) -> Result<$ty, D::Error>
495                where
496                    D: serde::Deserializer<'de>,
497                {
498                    super::deserialize_from_str(deserializer)
499                }
500
501                pub mod option {
502                    pub fn serialize<S>(
503                        value: &Option<$ty>,
504                        serializer: S,
505                    ) -> Result<S::Ok, S::Error>
506                    where
507                        S: serde::Serializer,
508                    {
509                        super::super::serialize_option_display(value.as_ref(), serializer)
510                    }
511
512                    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<$ty>, D::Error>
513                    where
514                        D: serde::Deserializer<'de>,
515                    {
516                        super::super::deserialize_option_from_str(deserializer)
517                    }
518                }
519            }
520        };
521    }
522
523    macro_rules! string_float_module {
524        ($module:ident, $ty:ty) => {
525            pub mod $module {
526                use serde::Deserialize;
527                use serde::de::Error as DeError;
528
529                pub fn serialize<S>(value: &$ty, serializer: S) -> Result<S::Ok, S::Error>
530                where
531                    S: serde::Serializer,
532                {
533                    super::serialize_display(value, serializer)
534                }
535
536                pub fn deserialize<'de, D>(deserializer: D) -> Result<$ty, D::Error>
537                where
538                    D: serde::Deserializer<'de>,
539                {
540                    let value = <String as Deserialize>::deserialize(deserializer)?;
541                    fast_float2::parse::<$ty, _>(&value).map_err(DeError::custom)
542                }
543
544                pub mod option {
545                    use serde::Deserialize;
546                    use serde::de::Error as DeError;
547
548                    pub fn serialize<S>(
549                        value: &Option<$ty>,
550                        serializer: S,
551                    ) -> Result<S::Ok, S::Error>
552                    where
553                        S: serde::Serializer,
554                    {
555                        super::super::serialize_option_display(value.as_ref(), serializer)
556                    }
557
558                    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<$ty>, D::Error>
559                    where
560                        D: serde::Deserializer<'de>,
561                    {
562                        let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
563                        value
564                            .map(|value| {
565                                fast_float2::parse::<$ty, _>(&value).map_err(DeError::custom)
566                            })
567                            .transpose()
568                    }
569                }
570            }
571        };
572    }
573
574    string_from_str_module!(as_u8, u8);
575    string_from_str_module!(as_u16, u16);
576    string_from_str_module!(as_u32, u32);
577    string_from_str_module!(as_u64, u64);
578    string_from_str_module!(as_i8, i8);
579    string_from_str_module!(as_i16, i16);
580    string_from_str_module!(as_i32, i32);
581    string_from_str_module!(as_i64, i64);
582    string_float_module!(as_f32, f32);
583    string_float_module!(as_f64, f64);
584
585    pub mod as_bool {
586        use std::fmt;
587
588        use serde::de::{Error as DeError, Visitor};
589
590        pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
591        where
592            S: serde::Serializer,
593        {
594            serializer.serialize_str(crate::format_bool(value))
595        }
596
597        pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
598        where
599            D: serde::Deserializer<'de>,
600        {
601            deserializer.deserialize_any(BoolVisitor)
602        }
603
604        struct BoolVisitor;
605
606        impl Visitor<'_> for BoolVisitor {
607            type Value = bool;
608
609            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
610                formatter.write_str("a boolean string or numeric boolean")
611            }
612
613            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
614                Ok(value)
615            }
616
617            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
618            where
619                E: DeError,
620            {
621                super::deserialize_bool(value).map_err(DeError::custom)
622            }
623
624            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
625            where
626                E: DeError,
627            {
628                self.visit_str(&value)
629            }
630
631            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
632            where
633                E: DeError,
634            {
635                match value {
636                    0 => Ok(false),
637                    1 => Ok(true),
638                    _ => Err(DeError::custom("invalid boolean number")),
639                }
640            }
641
642            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
643            where
644                E: DeError,
645            {
646                match value {
647                    0 => Ok(false),
648                    1 => Ok(true),
649                    _ => Err(DeError::custom("invalid boolean number")),
650                }
651            }
652        }
653
654        pub mod option {
655            use std::fmt;
656
657            use serde::de::Visitor;
658
659            pub fn serialize<S>(value: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error>
660            where
661                S: serde::Serializer,
662            {
663                match value {
664                    Some(value) => super::serialize(value, serializer),
665                    None => serializer.serialize_none(),
666                }
667            }
668
669            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
670            where
671                D: serde::Deserializer<'de>,
672            {
673                deserializer.deserialize_option(BoolOptionVisitor)
674            }
675
676            struct BoolOptionVisitor;
677
678            impl<'de> Visitor<'de> for BoolOptionVisitor {
679                type Value = Option<bool>;
680
681                fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
682                    formatter.write_str("an optional boolean string or numeric boolean")
683                }
684
685                fn visit_none<E>(self) -> Result<Self::Value, E> {
686                    Ok(None)
687                }
688
689                fn visit_unit<E>(self) -> Result<Self::Value, E> {
690                    Ok(None)
691                }
692
693                fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
694                where
695                    D: serde::Deserializer<'de>,
696                {
697                    super::deserialize(deserializer).map(Some)
698                }
699            }
700        }
701    }
702
703    pub mod as_date {
704        use serde::Deserialize;
705        use serde::de::Error as DeError;
706
707        use crate::Date;
708
709        pub fn serialize<S>(value: &Date, serializer: S) -> Result<S::Ok, S::Error>
710        where
711            S: serde::Serializer,
712        {
713            serializer.serialize_str(&crate::format_date(value))
714        }
715
716        pub fn deserialize<'de, D>(deserializer: D) -> Result<Date, D::Error>
717        where
718            D: serde::Deserializer<'de>,
719        {
720            let value = <String as Deserialize>::deserialize(deserializer)?;
721            crate::parse_date(&value).map_err(DeError::custom)
722        }
723
724        pub mod option {
725            use serde::Deserialize;
726            use serde::de::Error as DeError;
727
728            use crate::Date;
729
730            pub fn serialize<S>(value: &Option<Date>, serializer: S) -> Result<S::Ok, S::Error>
731            where
732                S: serde::Serializer,
733            {
734                match value {
735                    Some(value) => super::serialize(value, serializer),
736                    None => serializer.serialize_none(),
737                }
738            }
739
740            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Date>, D::Error>
741            where
742                D: serde::Deserializer<'de>,
743            {
744                let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
745                value
746                    .map(|value| crate::parse_date(&value).map_err(DeError::custom))
747                    .transpose()
748            }
749        }
750    }
751
752    pub mod as_naive_datetime {
753        use serde::Deserialize;
754        use serde::de::Error as DeError;
755
756        use crate::PrimitiveDateTime;
757
758        pub fn serialize<S>(value: &PrimitiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
759        where
760            S: serde::Serializer,
761        {
762            serializer.serialize_str(&crate::format_naive_datetime(value))
763        }
764
765        pub fn deserialize<'de, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
766        where
767            D: serde::Deserializer<'de>,
768        {
769            let value = <String as Deserialize>::deserialize(deserializer)?;
770            crate::parse_naive_datetime(&value).map_err(DeError::custom)
771        }
772
773        pub mod option {
774            use serde::Deserialize;
775            use serde::de::Error as DeError;
776
777            use crate::PrimitiveDateTime;
778
779            pub fn serialize<S>(
780                value: &Option<PrimitiveDateTime>,
781                serializer: S,
782            ) -> Result<S::Ok, S::Error>
783            where
784                S: serde::Serializer,
785            {
786                match value {
787                    Some(value) => super::serialize(value, serializer),
788                    None => serializer.serialize_none(),
789                }
790            }
791
792            pub fn deserialize<'de, D>(
793                deserializer: D,
794            ) -> Result<Option<PrimitiveDateTime>, D::Error>
795            where
796                D: serde::Deserializer<'de>,
797            {
798                let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
799                value
800                    .map(|value| crate::parse_naive_datetime(&value).map_err(DeError::custom))
801                    .transpose()
802            }
803        }
804    }
805
806    pub mod as_offset_datetime {
807        use serde::Deserialize;
808        use serde::de::Error as DeError;
809        use serde::ser::Error as SerError;
810
811        use crate::OffsetDateTime;
812        use time::format_description::well_known::Rfc3339;
813
814        pub fn serialize<S>(value: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
815        where
816            S: serde::Serializer,
817        {
818            let value = value.format(&Rfc3339).map_err(SerError::custom)?;
819            serializer.serialize_str(&value)
820        }
821
822        pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
823        where
824            D: serde::Deserializer<'de>,
825        {
826            let value = <String as Deserialize>::deserialize(deserializer)?;
827            OffsetDateTime::parse(&value, &Rfc3339).map_err(DeError::custom)
828        }
829
830        pub mod option {
831            use serde::Deserialize;
832            use serde::de::Error as DeError;
833
834            use crate::OffsetDateTime;
835            use time::format_description::well_known::Rfc3339;
836
837            pub fn serialize<S>(
838                value: &Option<OffsetDateTime>,
839                serializer: S,
840            ) -> Result<S::Ok, S::Error>
841            where
842                S: serde::Serializer,
843            {
844                match value {
845                    Some(value) => super::serialize(value, serializer),
846                    None => serializer.serialize_none(),
847                }
848            }
849
850            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
851            where
852                D: serde::Deserializer<'de>,
853            {
854                let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
855                value
856                    .map(|value| OffsetDateTime::parse(&value, &Rfc3339).map_err(DeError::custom))
857                    .transpose()
858            }
859        }
860    }
861
862    pub mod as_unix_time {
863        use serde::Deserialize;
864        use serde::de::Error as DeError;
865
866        use crate::OffsetDateTime;
867
868        pub fn serialize<S>(value: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
869        where
870            S: serde::Serializer,
871        {
872            serializer.serialize_str(&crate::format_unix_time(value))
873        }
874
875        pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
876        where
877            D: serde::Deserializer<'de>,
878        {
879            let value = <String as Deserialize>::deserialize(deserializer)?;
880            let value = value.parse::<i64>().map_err(DeError::custom)?;
881            OffsetDateTime::from_unix_timestamp(value).map_err(DeError::custom)
882        }
883
884        pub mod option {
885            use serde::Deserialize;
886            use serde::de::Error as DeError;
887
888            use crate::OffsetDateTime;
889
890            pub fn serialize<S>(
891                value: &Option<OffsetDateTime>,
892                serializer: S,
893            ) -> Result<S::Ok, S::Error>
894            where
895                S: serde::Serializer,
896            {
897                match value {
898                    Some(value) => super::serialize(value, serializer),
899                    None => serializer.serialize_none(),
900                }
901            }
902
903            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
904            where
905                D: serde::Deserializer<'de>,
906            {
907                let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
908                value
909                    .map(|value| {
910                        let value = value.parse::<i64>().map_err(DeError::custom)?;
911                        OffsetDateTime::from_unix_timestamp(value).map_err(DeError::custom)
912                    })
913                    .transpose()
914            }
915        }
916    }
917
918    pub mod as_time {
919        use serde::Deserialize;
920        use serde::de::Error as DeError;
921
922        use crate::Time;
923
924        pub fn serialize<S>(value: &Time, serializer: S) -> Result<S::Ok, S::Error>
925        where
926            S: serde::Serializer,
927        {
928            serializer.serialize_str(&crate::format_time(value))
929        }
930
931        pub fn deserialize<'de, D>(deserializer: D) -> Result<Time, D::Error>
932        where
933            D: serde::Deserializer<'de>,
934        {
935            let value = <String as Deserialize>::deserialize(deserializer)?;
936            crate::parse_time(&value).map_err(DeError::custom)
937        }
938
939        pub mod option {
940            use serde::Deserialize;
941            use serde::de::Error as DeError;
942
943            use crate::Time;
944
945            pub fn serialize<S>(value: &Option<Time>, serializer: S) -> Result<S::Ok, S::Error>
946            where
947                S: serde::Serializer,
948            {
949                match value {
950                    Some(value) => super::serialize(value, serializer),
951                    None => serializer.serialize_none(),
952                }
953            }
954
955            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Time>, D::Error>
956            where
957                D: serde::Deserializer<'de>,
958            {
959                let value = <Option<String> as Deserialize>::deserialize(deserializer)?;
960                let Some(value) = value else {
961                    return Ok(None);
962                };
963                let value = value.trim();
964                if value.is_empty() {
965                    return Ok(None);
966                }
967                crate::parse_time(value).map(Some).map_err(DeError::custom)
968            }
969        }
970    }
971
972    fn serialize_display<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
973    where
974        T: fmt::Display,
975        S: serde::Serializer,
976    {
977        serializer.serialize_str(&value.to_string())
978    }
979
980    fn serialize_option_display<T, S>(value: Option<&T>, serializer: S) -> Result<S::Ok, S::Error>
981    where
982        T: fmt::Display,
983        S: serde::Serializer,
984    {
985        match value {
986            Some(value) => serialize_display(value, serializer),
987            None => serializer.serialize_none(),
988        }
989    }
990
991    fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
992    where
993        T: FromStr,
994        T::Err: fmt::Display,
995        D: serde::Deserializer<'de>,
996    {
997        let value = String::deserialize(deserializer)?;
998        value.parse::<T>().map_err(DeError::custom)
999    }
1000
1001    fn deserialize_option_from_str<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
1002    where
1003        T: FromStr,
1004        T::Err: fmt::Display,
1005        D: serde::Deserializer<'de>,
1006    {
1007        let value = Option::<String>::deserialize(deserializer)?;
1008        value
1009            .map(|value| value.parse::<T>().map_err(DeError::custom))
1010            .transpose()
1011    }
1012
1013    fn deserialize_bool(value: &str) -> Result<bool, &'static str> {
1014        match value {
1015            "1" => Ok(true),
1016            "0" => Ok(false),
1017            value if value.eq_ignore_ascii_case("true") => Ok(true),
1018            value if value.eq_ignore_ascii_case("false") => Ok(false),
1019            _ => Err("invalid boolean string"),
1020        }
1021    }
1022}
1023
1024#[cfg(feature = "serde")]
1025#[allow(clippy::missing_errors_doc)]
1026pub mod serde_integer {
1027    pub mod as_unix_time {
1028        use serde::Deserialize;
1029        use serde::de::Error as DeError;
1030
1031        use crate::OffsetDateTime;
1032
1033        pub fn serialize<S>(value: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1034        where
1035            S: serde::Serializer,
1036        {
1037            serializer.serialize_i64(value.unix_timestamp())
1038        }
1039
1040        pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
1041        where
1042            D: serde::Deserializer<'de>,
1043        {
1044            let value = <i64 as Deserialize>::deserialize(deserializer)?;
1045            OffsetDateTime::from_unix_timestamp(value).map_err(DeError::custom)
1046        }
1047
1048        pub mod option {
1049            use serde::Deserialize;
1050            use serde::de::Error as DeError;
1051
1052            use crate::OffsetDateTime;
1053
1054            pub fn serialize<S>(
1055                value: &Option<OffsetDateTime>,
1056                serializer: S,
1057            ) -> Result<S::Ok, S::Error>
1058            where
1059                S: serde::Serializer,
1060            {
1061                match value {
1062                    Some(value) => super::serialize(value, serializer),
1063                    None => serializer.serialize_none(),
1064                }
1065            }
1066
1067            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
1068            where
1069                D: serde::Deserializer<'de>,
1070            {
1071                let value = <Option<i64> as Deserialize>::deserialize(deserializer)?;
1072                value
1073                    .map(|value| {
1074                        OffsetDateTime::from_unix_timestamp(value).map_err(DeError::custom)
1075                    })
1076                    .transpose()
1077            }
1078        }
1079    }
1080
1081    pub mod as_bool {
1082        use crate::serde_string::as_bool as string_bool;
1083
1084        pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
1085        where
1086            S: serde::Serializer,
1087        {
1088            serializer.serialize_u8(u8::from(*value))
1089        }
1090
1091        pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
1092        where
1093            D: serde::Deserializer<'de>,
1094        {
1095            string_bool::deserialize(deserializer)
1096        }
1097
1098        pub mod option {
1099            use crate::serde_string::as_bool::option as string_bool_option;
1100
1101            pub fn serialize<S>(value: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error>
1102            where
1103                S: serde::Serializer,
1104            {
1105                match value {
1106                    Some(value) => super::serialize(value, serializer),
1107                    None => serializer.serialize_none(),
1108                }
1109            }
1110
1111            pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
1112            where
1113                D: serde::Deserializer<'de>,
1114            {
1115                string_bool_option::deserialize(deserializer)
1116            }
1117        }
1118    }
1119}
1120
1121#[cfg(feature = "json")]
1122pub mod treat_error_as_none {
1123    use serde::de::DeserializeOwned;
1124    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1125
1126    /// Serializes an optional value, omitting `None`.
1127    ///
1128    /// # Errors
1129    ///
1130    /// Returns an error if serializing the inner value fails.
1131    pub fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
1132    where
1133        S: Serializer,
1134        T: Serialize,
1135    {
1136        match value {
1137            Some(inner) => inner.serialize(serializer),
1138            None => serializer.serialize_none(),
1139        }
1140    }
1141
1142    /// Deserializes an optional value, treating invalid inner values as `None`.
1143    ///
1144    /// # Errors
1145    ///
1146    /// Returns an error if the input cannot be read as JSON.
1147    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1148    where
1149        D: Deserializer<'de>,
1150        T: DeserializeOwned,
1151    {
1152        let value = serde_json::Value::deserialize(deserializer)?;
1153        match T::deserialize(value) {
1154            Ok(parsed) => Ok(Some(parsed)),
1155            Err(_) => Ok(None),
1156        }
1157    }
1158}
1159
1160/// Inserts a header into a header map.
1161///
1162/// # Errors
1163///
1164/// Returns an error if the header name or value is invalid.
1165pub fn insert_header(
1166    headers: &mut http::HeaderMap,
1167    name: &'static str,
1168    value: &str,
1169) -> Result<(), Error> {
1170    headers.insert(
1171        HeaderName::from_bytes(name.as_bytes())?,
1172        HeaderValue::from_str(value)?,
1173    );
1174    Ok(())
1175}
1176
1177#[must_use]
1178pub fn has_json_content_type(headers: &http::HeaderMap) -> bool {
1179    headers
1180        .get(CONTENT_TYPE)
1181        .and_then(|value| value.to_str().ok())
1182        .is_some_and(is_json_media_type)
1183}
1184
1185fn is_json_media_type(value: &str) -> bool {
1186    let media_type = value.split(';').next().unwrap_or(value).trim();
1187    if media_type.eq_ignore_ascii_case("application/json") {
1188        return true;
1189    }
1190
1191    let Some((_, subtype)) = media_type.rsplit_once('/') else {
1192        return false;
1193    };
1194    ends_with_ignore_ascii_case(subtype, "+json")
1195}
1196
1197fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool {
1198    let value = value.as_bytes();
1199    let suffix = suffix.as_bytes();
1200    value.len() >= suffix.len() && value[value.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
1201}
1202
1203fn append_percent_encoded(out: &mut String, bytes: &[u8]) {
1204    const HEX: &[u8; 16] = b"0123456789ABCDEF";
1205
1206    for &byte in bytes {
1207        if is_unreserved(byte) {
1208            out.push(byte as char);
1209        } else {
1210            out.push('%');
1211            out.push(HEX[(byte >> 4) as usize] as char);
1212            out.push(HEX[(byte & 0x0f) as usize] as char);
1213        }
1214    }
1215}
1216
1217const fn is_unreserved(byte: u8) -> bool {
1218    matches!(
1219        byte,
1220        b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
1221    )
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227
1228    #[test]
1229    fn encodes_path_segments() {
1230        let mut out = String::new();
1231        append_path_segment(&mut out, "a/b c");
1232        assert_eq!(out, "a%2Fb%20c");
1233    }
1234
1235    #[test]
1236    fn appends_query_pairs() {
1237        let mut out = String::from("/pets");
1238        let mut first = true;
1239        append_query_pair(&mut out, &mut first, "tag name", "small/dog");
1240        append_query_pair(&mut out, &mut first, "limit", "10");
1241        assert_eq!(out, "/pets?tag%20name=small%2Fdog&limit=10");
1242    }
1243
1244    #[test]
1245    fn parses_range_strings() {
1246        assert_eq!(parse_range::<u8>("14-17").unwrap(), (Some(14), Some(17)));
1247        assert_eq!(parse_range::<u8>("14-").unwrap(), (Some(14), None));
1248        assert_eq!(parse_range::<u8>("-17").unwrap(), (None, Some(17)));
1249        assert_eq!(parse_range::<u8>("").unwrap(), (None, None));
1250        assert!(matches!(
1251            parse_range::<u8>("14-17-20"),
1252            Err(ParseRangeError::TooManySeparators)
1253        ));
1254    }
1255
1256    #[test]
1257    fn formats_range_strings() {
1258        assert_eq!(format_range(&Some(14), &Some(17)), "14-17");
1259        assert_eq!(format_range(&Some(14), &None::<u8>), "14-");
1260        assert_eq!(format_range(&None::<u8>, &Some(17)), "-17");
1261        assert_eq!(format_range(&None::<u8>, &None::<u8>), "");
1262    }
1263
1264    #[test]
1265    fn parses_and_formats_time_strings() {
1266        let time = parse_time("0620").unwrap();
1267        assert_eq!(time.hour(), 6);
1268        assert_eq!(time.minute(), 20);
1269        assert_eq!(format_time(&time), "0620");
1270        assert_eq!(parse_time("6:20"), Err(ParseTimeError::InvalidFormat));
1271        assert_eq!(parse_time("2400"), Err(ParseTimeError::ComponentRange));
1272    }
1273
1274    #[test]
1275    fn parses_and_formats_date_strings() {
1276        let date = parse_date("2024-07-16").unwrap();
1277        assert_eq!(
1278            date,
1279            Date::from_calendar_date(2024, Month::July, 16).unwrap()
1280        );
1281        assert_eq!(format_date(&date), "2024-07-16");
1282        assert_eq!(parse_date("2024-7-16"), Err(ParseDateError::InvalidFormat));
1283        assert_eq!(parse_date("not-a-date"), Err(ParseDateError::InvalidFormat));
1284    }
1285
1286    #[test]
1287    fn parses_and_formats_naive_datetime_strings() {
1288        let datetime = parse_naive_datetime("2024-07-16T23:59:00").unwrap();
1289        assert_eq!(datetime.hour(), 23);
1290        assert_eq!(datetime.minute(), 59);
1291        assert_eq!(datetime.second(), 0);
1292        assert_eq!(format_naive_datetime(&datetime), "2024-07-16T23:59:00");
1293        assert_eq!(
1294            parse_naive_datetime("2024-07-16T23:59"),
1295            Err(ParseNaiveDateTimeError::InvalidFormat)
1296        );
1297        assert_eq!(
1298            parse_naive_datetime("2024-07-16 23:59:00"),
1299            Err(ParseNaiveDateTimeError::InvalidFormat)
1300        );
1301    }
1302
1303    #[test]
1304    fn formats_unix_time_seconds() {
1305        let datetime = OffsetDateTime::from_unix_timestamp(-1).unwrap();
1306        assert_eq!(format_unix_time(&datetime), "-1");
1307    }
1308
1309    #[cfg(all(feature = "serde", feature = "json"))]
1310    #[test]
1311    fn serde_string_naive_datetime_round_trips() {
1312        #[derive(serde::Deserialize, serde::Serialize)]
1313        struct Value {
1314            #[serde(with = "crate::serde_string::as_naive_datetime")]
1315            at: PrimitiveDateTime,
1316        }
1317
1318        let at = parse_naive_datetime("2024-07-16T23:59:00").unwrap();
1319        let encoded = serde_json::to_value(Value { at }).unwrap();
1320        assert_eq!(encoded, serde_json::json!({ "at": "2024-07-16T23:59:00" }));
1321
1322        let decoded = serde_json::from_value::<Value>(encoded).unwrap();
1323        assert_eq!(decoded.at, at);
1324    }
1325
1326    #[cfg(all(feature = "serde", feature = "json"))]
1327    #[test]
1328    fn serde_string_date_round_trips() {
1329        #[derive(serde::Deserialize, serde::Serialize)]
1330        struct Value {
1331            #[serde(with = "crate::serde_string::as_date")]
1332            day: Date,
1333        }
1334
1335        let date = Date::from_calendar_date(2024, Month::July, 16).unwrap();
1336        let encoded = serde_json::to_value(Value { day: date }).unwrap();
1337        assert_eq!(encoded, serde_json::json!({ "day": "2024-07-16" }));
1338
1339        let decoded = serde_json::from_value::<Value>(encoded).unwrap();
1340        assert_eq!(decoded.day, date);
1341    }
1342
1343    #[test]
1344    fn recognizes_json_content_types() {
1345        let mut headers = http::HeaderMap::new();
1346        headers.insert(
1347            CONTENT_TYPE,
1348            http::HeaderValue::from_static("application/problem+json; charset=utf-8"),
1349        );
1350        assert!(has_json_content_type(&headers));
1351    }
1352
1353    #[test]
1354    fn response_parts_holds_status_headers_body() {
1355        let mut headers = http::HeaderMap::new();
1356        headers.insert(
1357            CONTENT_TYPE,
1358            http::HeaderValue::from_static("application/json"),
1359        );
1360        let body = br#"{"ok":true}"#.to_vec();
1361        let parts = ResponseParts {
1362            status: http::StatusCode::OK,
1363            headers,
1364            body,
1365        };
1366        assert_eq!(parts.status, http::StatusCode::OK);
1367        assert_eq!(parts.headers.get(CONTENT_TYPE).unwrap(), "application/json");
1368        assert_eq!(parts.body, br#"{"ok":true}"#);
1369    }
1370
1371    #[cfg(all(feature = "serde", feature = "json"))]
1372    #[test]
1373    fn serde_string_bool_accepts_string_and_numeric_values() {
1374        #[derive(serde::Deserialize, serde::Serialize)]
1375        struct Value {
1376            #[serde(with = "crate::serde_string::as_bool")]
1377            monitored: bool,
1378        }
1379
1380        let numeric = serde_json::from_str::<Value>(r#"{"monitored":0}"#).unwrap();
1381        assert!(!numeric.monitored);
1382
1383        let string = serde_json::from_str::<Value>(r#"{"monitored":"1"}"#).unwrap();
1384        assert!(string.monitored);
1385
1386        let encoded = serde_json::to_value(Value { monitored: false }).unwrap();
1387        assert_eq!(encoded, serde_json::json!({ "monitored": "0" }));
1388    }
1389
1390    #[cfg(all(feature = "serde", feature = "json"))]
1391    #[test]
1392    fn serde_integer_bool_accepts_numeric_values() {
1393        #[derive(serde::Deserialize, serde::Serialize)]
1394        struct Value {
1395            #[serde(with = "crate::serde_integer::as_bool")]
1396            monitored: bool,
1397        }
1398
1399        let numeric = serde_json::from_str::<Value>(r#"{"monitored":0}"#).unwrap();
1400        assert!(!numeric.monitored);
1401
1402        let encoded = serde_json::to_value(Value { monitored: true }).unwrap();
1403        assert_eq!(encoded, serde_json::json!({ "monitored": 1 }));
1404    }
1405
1406    #[cfg(all(feature = "serde", feature = "json"))]
1407    #[test]
1408    fn serde_string_unix_time_round_trips() {
1409        #[derive(serde::Deserialize, serde::Serialize)]
1410        struct Value {
1411            #[serde(with = "crate::serde_string::as_unix_time")]
1412            at: OffsetDateTime,
1413        }
1414
1415        let at = OffsetDateTime::from_unix_timestamp(1_719_892_800).unwrap();
1416        let encoded = serde_json::to_value(Value { at }).unwrap();
1417        assert_eq!(encoded, serde_json::json!({ "at": "1719892800" }));
1418
1419        let decoded = serde_json::from_value::<Value>(encoded).unwrap();
1420        assert_eq!(decoded.at, at);
1421    }
1422
1423    #[cfg(all(feature = "serde", feature = "json"))]
1424    #[test]
1425    fn serde_integer_unix_time_round_trips_and_handles_null() {
1426        #[derive(serde::Deserialize, serde::Serialize)]
1427        struct Value {
1428            #[serde(with = "crate::serde_integer::as_unix_time")]
1429            at: OffsetDateTime,
1430            #[serde(with = "crate::serde_integer::as_unix_time::option")]
1431            maybe_at: Option<OffsetDateTime>,
1432        }
1433
1434        let at = OffsetDateTime::from_unix_timestamp(1_719_892_800).unwrap();
1435        let encoded = serde_json::to_value(Value { at, maybe_at: None }).unwrap();
1436        assert_eq!(
1437            encoded,
1438            serde_json::json!({ "at": 1_719_892_800, "maybe_at": null })
1439        );
1440
1441        let decoded = serde_json::from_value::<Value>(encoded).unwrap();
1442        assert_eq!(decoded.at, at);
1443        assert_eq!(decoded.maybe_at, None);
1444    }
1445
1446    #[cfg(all(feature = "serde", feature = "json"))]
1447    #[test]
1448    fn serde_integer_unix_time_rejects_out_of_range_values() {
1449        #[derive(serde::Deserialize)]
1450        #[allow(dead_code)]
1451        struct Value {
1452            #[serde(with = "crate::serde_integer::as_unix_time")]
1453            at: OffsetDateTime,
1454        }
1455
1456        assert!(serde_json::from_str::<Value>(r#"{"at":9223372036854775807}"#).is_err());
1457    }
1458}