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