relay_event_schema/protocol/
request.rs

1use cookie::Cookie;
2use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Object, Value};
3use url::form_urlencoded;
4
5use crate::processor::ProcessValue;
6use crate::protocol::{JsonLenientString, LenientString, PairList};
7
8type CookieEntry = Annotated<(Annotated<String>, Annotated<String>)>;
9
10/// A map holding cookies.
11#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
12pub struct Cookies(pub PairList<(Annotated<String>, Annotated<String>)>);
13
14impl Cookies {
15    pub fn parse(string: &str) -> Result<Self, Error> {
16        let pairs: Result<_, _> = Self::iter_cookies(string).collect();
17        pairs.map(Cookies)
18    }
19
20    fn iter_cookies(string: &str) -> impl Iterator<Item = Result<CookieEntry, Error>> + '_ {
21        string
22            .split(';')
23            .filter(|cookie| !cookie.trim().is_empty())
24            .map(Cookies::parse_cookie)
25    }
26
27    fn parse_cookie(string: &str) -> Result<CookieEntry, Error> {
28        match Cookie::parse_encoded(string) {
29            Ok(cookie) => Ok(Annotated::from((
30                cookie.name().to_string().into(),
31                cookie.value().to_string().into(),
32            ))),
33            Err(error) => Err(Error::invalid(error)),
34        }
35    }
36}
37
38impl std::ops::Deref for Cookies {
39    type Target = PairList<(Annotated<String>, Annotated<String>)>;
40
41    fn deref(&self) -> &Self::Target {
42        &self.0
43    }
44}
45
46impl std::ops::DerefMut for Cookies {
47    fn deref_mut(&mut self) -> &mut Self::Target {
48        &mut self.0
49    }
50}
51
52impl FromValue for Cookies {
53    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
54        match value {
55            Annotated(Some(Value::String(value)), mut meta) => {
56                let mut cookies = Vec::new();
57                for result in Cookies::iter_cookies(&value) {
58                    match result {
59                        Ok(cookie) => cookies.push(cookie),
60                        Err(error) => meta.add_error(error),
61                    }
62                }
63
64                if meta.has_errors() && meta.original_value().is_none() {
65                    meta.set_original_value(Some(value));
66                }
67
68                Annotated(Some(Cookies(PairList(cookies))), meta)
69            }
70            annotated @ Annotated(Some(Value::Object(_)), _)
71            | annotated @ Annotated(Some(Value::Array(_)), _) => {
72                PairList::from_value(annotated).map_value(Cookies)
73            }
74            Annotated(None, meta) => Annotated(None, meta),
75            Annotated(Some(value), mut meta) => {
76                meta.add_error(Error::expected("cookies"));
77                meta.set_original_value(Some(value));
78                Annotated(None, meta)
79            }
80        }
81    }
82}
83
84/// A "into-string" type that normalizes header names.
85#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, IntoValue, ProcessValue)]
86#[metastructure(process_func = "process_header_name")]
87pub struct HeaderName(String);
88
89impl HeaderName {
90    /// Creates a normalized header name.
91    pub fn new<S: AsRef<str>>(name: S) -> Self {
92        let name = name.as_ref();
93        let mut normalized = String::with_capacity(name.len());
94
95        name.chars().fold(true, |uppercase, c| {
96            if uppercase {
97                normalized.extend(c.to_uppercase());
98            } else {
99                normalized.push(c); // does not lowercase on purpose
100            }
101            c == '-'
102        });
103
104        HeaderName(normalized)
105    }
106
107    /// Returns the string value.
108    pub fn as_str(&self) -> &str {
109        &self.0
110    }
111
112    /// Unwraps the inner raw string.
113    pub fn into_inner(self) -> String {
114        self.0
115    }
116}
117
118impl AsRef<str> for HeaderName {
119    fn as_ref(&self) -> &str {
120        &self.0
121    }
122}
123
124impl std::ops::Deref for HeaderName {
125    type Target = String;
126
127    fn deref(&self) -> &Self::Target {
128        &self.0
129    }
130}
131
132impl std::ops::DerefMut for HeaderName {
133    fn deref_mut(&mut self) -> &mut Self::Target {
134        &mut self.0
135    }
136}
137
138impl From<String> for HeaderName {
139    fn from(value: String) -> Self {
140        HeaderName::new(value)
141    }
142}
143
144impl From<&'_ str> for HeaderName {
145    fn from(value: &str) -> Self {
146        HeaderName::new(value)
147    }
148}
149
150impl FromValue for HeaderName {
151    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
152        String::from_value(value).map_value(HeaderName::new)
153    }
154}
155
156/// A "into-string" type that normalizes header values.
157#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, IntoValue, ProcessValue)]
158pub struct HeaderValue(String);
159
160impl HeaderValue {
161    pub fn new<S: AsRef<str>>(value: S) -> Self {
162        HeaderValue(value.as_ref().to_owned())
163    }
164}
165
166impl AsRef<str> for HeaderValue {
167    fn as_ref(&self) -> &str {
168        &self.0
169    }
170}
171
172impl std::ops::Deref for HeaderValue {
173    type Target = String;
174
175    fn deref(&self) -> &Self::Target {
176        &self.0
177    }
178}
179
180impl std::ops::DerefMut for HeaderValue {
181    fn deref_mut(&mut self) -> &mut Self::Target {
182        &mut self.0
183    }
184}
185
186impl From<String> for HeaderValue {
187    fn from(value: String) -> Self {
188        HeaderValue::new(value)
189    }
190}
191
192impl From<&'_ str> for HeaderValue {
193    fn from(value: &str) -> Self {
194        HeaderValue::new(value)
195    }
196}
197
198impl FromValue for HeaderValue {
199    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
200        match value {
201            Annotated(Some(Value::Array(array)), mut meta) => {
202                let mut header_value = String::new();
203                for array_value in array {
204                    let array_value = LenientString::from_value(array_value);
205                    for error in array_value.meta().iter_errors() {
206                        meta.add_error(error.clone());
207                    }
208
209                    if let Some(string) = array_value.value() {
210                        if !header_value.is_empty() {
211                            header_value.push(',');
212                        }
213
214                        header_value.push_str(string);
215                    }
216                }
217
218                Annotated(Some(HeaderValue::new(header_value)), meta)
219            }
220            annotated => LenientString::from_value(annotated).map_value(HeaderValue::new),
221        }
222    }
223}
224
225/// A map holding headers.
226#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
227pub struct Headers(pub PairList<(Annotated<HeaderName>, Annotated<HeaderValue>)>);
228
229impl Headers {
230    pub fn get_header(&self, key: &str) -> Option<&str> {
231        for item in self.iter() {
232            if let Some((ref k, ref v)) = item.value() {
233                if k.as_str() == Some(key) {
234                    return v.as_str();
235                }
236            }
237        }
238
239        None
240    }
241}
242
243impl std::ops::Deref for Headers {
244    type Target = PairList<(Annotated<HeaderName>, Annotated<HeaderValue>)>;
245
246    fn deref(&self) -> &Self::Target {
247        &self.0
248    }
249}
250
251impl std::ops::DerefMut for Headers {
252    fn deref_mut(&mut self) -> &mut Self::Target {
253        &mut self.0
254    }
255}
256
257impl FromValue for Headers {
258    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
259        // Preserve order if SDK sent headers as array
260        let should_sort = matches!(value.value(), Some(Value::Object(_)));
261
262        type HeaderTuple = (Annotated<HeaderName>, Annotated<HeaderValue>);
263        PairList::<HeaderTuple>::from_value(value).map_value(|mut pair_list| {
264            if should_sort {
265                pair_list.sort_unstable_by(|a, b| {
266                    a.value()
267                        .map(|x| x.0.value())
268                        .cmp(&b.value().map(|x| x.0.value()))
269                });
270            }
271
272            Headers(pair_list)
273        })
274    }
275}
276
277/// A map holding query string pairs.
278#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
279pub struct Query(pub PairList<(Annotated<String>, Annotated<JsonLenientString>)>);
280
281impl Query {
282    pub fn parse(mut string: &str) -> Self {
283        if string.starts_with('?') {
284            string = &string[1..];
285        }
286
287        form_urlencoded::parse(string.as_bytes()).collect()
288    }
289}
290
291impl std::ops::Deref for Query {
292    type Target = PairList<(Annotated<String>, Annotated<JsonLenientString>)>;
293
294    fn deref(&self) -> &Self::Target {
295        &self.0
296    }
297}
298
299impl std::ops::DerefMut for Query {
300    fn deref_mut(&mut self) -> &mut Self::Target {
301        &mut self.0
302    }
303}
304
305impl<K, V> FromIterator<(K, V)> for Query
306where
307    K: Into<String>,
308    V: Into<String>,
309{
310    fn from_iter<T>(iter: T) -> Self
311    where
312        T: IntoIterator<Item = (K, V)>,
313    {
314        let pairs = iter.into_iter().map(|(key, value)| {
315            Annotated::new((
316                Annotated::new(key.into()),
317                Annotated::new(value.into().into()),
318            ))
319        });
320
321        Query(pairs.collect())
322    }
323}
324
325impl FromValue for Query {
326    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
327        match value {
328            Annotated(Some(Value::String(v)), meta) => Annotated(Some(Query::parse(&v)), meta),
329            annotated @ Annotated(Some(Value::Object(_)), _)
330            | annotated @ Annotated(Some(Value::Array(_)), _) => {
331                PairList::from_value(annotated).map_value(Query)
332            }
333            Annotated(None, meta) => Annotated(None, meta),
334            Annotated(Some(value), mut meta) => {
335                meta.add_error(Error::expected("a query string or map"));
336                meta.set_original_value(Some(value));
337                Annotated(None, meta)
338            }
339        }
340    }
341}
342
343/// Http request information.
344///
345/// The Request interface contains information on a HTTP request related to the event. In client
346/// SDKs, this can be an outgoing request, or the request that rendered the current web page. On
347/// server SDKs, this could be the incoming web request that is being handled.
348///
349/// The data variable should only contain the request body (not the query string). It can either be
350/// a dictionary (for standard HTTP requests) or a raw request body.
351///
352/// ### Ordered Maps
353///
354/// In the Request interface, several attributes can either be declared as string, object, or list
355/// of tuples. Sentry attempts to parse structured information from the string representation in
356/// such cases.
357///
358/// Sometimes, keys can be declared multiple times, or the order of elements matters. In such
359/// cases, use the tuple representation over a plain object.
360///
361/// Example of request headers as object:
362///
363/// ```json
364/// {
365///   "content-type": "application/json",
366///   "accept": "application/json, application/xml"
367/// }
368/// ```
369///
370/// Example of the same headers as list of tuples:
371///
372/// ```json
373/// [
374///   ["content-type", "application/json"],
375///   ["accept", "application/json"],
376///   ["accept", "application/xml"]
377/// ]
378/// ```
379///
380/// Example of a fully populated request object:
381///
382/// ```json
383/// {
384///   "request": {
385///     "method": "POST",
386///     "url": "http://absolute.uri/foo",
387///     "query_string": "query=foobar&page=2",
388///     "data": {
389///       "foo": "bar"
390///     },
391///     "cookies": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
392///     "headers": {
393///       "content-type": "text/html"
394///     },
395///     "env": {
396///       "REMOTE_ADDR": "192.168.0.1"
397///     }
398///   }
399/// }
400/// ```
401#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
402#[metastructure(process_func = "process_request", value_type = "Request")]
403pub struct Request {
404    /// The URL of the request if available.
405    ///
406    ///The query string can be declared either as part of the `url`, or separately in `query_string`.
407    #[metastructure(max_chars = 256, max_chars_allowance = 40, pii = "maybe")]
408    pub url: Annotated<String>,
409
410    /// HTTP request method.
411    pub method: Annotated<String>,
412
413    /// HTTP protocol.
414    pub protocol: Annotated<String>,
415
416    /// Request data in any format that makes sense.
417    ///
418    /// SDKs should discard large and binary bodies by default. Can be given as a string or
419    /// structural data of any format.
420    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
421    pub data: Annotated<Value>,
422
423    /// The query string component of the URL.
424    ///
425    /// Can be given as unparsed string, dictionary, or list of tuples.
426    ///
427    /// If the query string is not declared and part of the `url`, Sentry moves it to the
428    /// query string.
429    #[metastructure(pii = "true", max_depth = 3, max_bytes = 1024)]
430    #[metastructure(skip_serialization = "empty")]
431    pub query_string: Annotated<Query>,
432
433    /// The fragment of the request URI.
434    #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
435    #[metastructure(skip_serialization = "empty")]
436    pub fragment: Annotated<String>,
437
438    /// The cookie values.
439    ///
440    /// Can be given unparsed as string, as dictionary, or as a list of tuples.
441    #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
442    #[metastructure(skip_serialization = "empty")]
443    pub cookies: Annotated<Cookies>,
444
445    /// A dictionary of submitted headers.
446    ///
447    /// If a header appears multiple times it, needs to be merged according to the HTTP standard
448    /// for header merging. Header names are treated case-insensitively by Sentry.
449    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
450    #[metastructure(skip_serialization = "empty")]
451    pub headers: Annotated<Headers>,
452
453    /// HTTP request body size.
454    pub body_size: Annotated<u64>,
455
456    /// Server environment data, such as CGI/WSGI.
457    ///
458    /// A dictionary containing environment information passed from the server. This is where
459    /// information such as CGI/WSGI/Rack keys go that are not HTTP headers.
460    ///
461    /// Sentry will explicitly look for `REMOTE_ADDR` to extract an IP address.
462    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
463    #[metastructure(skip_serialization = "empty")]
464    pub env: Annotated<Object<Value>>,
465
466    /// The inferred content type of the request payload.
467    #[metastructure(skip_serialization = "empty")]
468    pub inferred_content_type: Annotated<String>,
469
470    /// The API target/specification that made the request.
471    ///
472    /// Values can be `graphql`, `rest`, etc.
473    ///
474    /// The data field should contain the request and response bodies based on its target specification.
475    ///
476    /// This information can be used for better data scrubbing and normalization.
477    pub api_target: Annotated<String>,
478
479    /// Additional arbitrary fields for forwards compatibility.
480    #[metastructure(additional_properties, pii = "true")]
481    pub other: Object<Value>,
482}
483
484#[cfg(test)]
485mod tests {
486    use similar_asserts::assert_eq;
487
488    use super::*;
489
490    #[test]
491    fn test_header_normalization() {
492        let json = r#"{
493  "-other-": "header",
494  "accept": "application/json",
495  "WWW-Authenticate": "basic",
496  "x-sentry": "version=8"
497}"#;
498
499        let headers = vec![
500            Annotated::new((
501                Annotated::new("-Other-".to_string().into()),
502                Annotated::new("header".to_string().into()),
503            )),
504            Annotated::new((
505                Annotated::new("Accept".to_string().into()),
506                Annotated::new("application/json".to_string().into()),
507            )),
508            Annotated::new((
509                Annotated::new("WWW-Authenticate".to_string().into()),
510                Annotated::new("basic".to_string().into()),
511            )),
512            Annotated::new((
513                Annotated::new("X-Sentry".to_string().into()),
514                Annotated::new("version=8".to_string().into()),
515            )),
516        ];
517
518        let headers = Annotated::new(Headers(PairList(headers)));
519        assert_eq!(headers, Annotated::from_json(json).unwrap());
520    }
521
522    #[test]
523    fn test_header_from_sequence() {
524        let json = r#"[
525  ["accept", "application/json"]
526]"#;
527
528        let headers = vec![Annotated::new((
529            Annotated::new("Accept".to_string().into()),
530            Annotated::new("application/json".to_string().into()),
531        ))];
532
533        let headers = Annotated::new(Headers(PairList(headers)));
534        assert_eq!(headers, Annotated::from_json(json).unwrap());
535
536        let json = r#"[
537  ["accept", "application/json"],
538  [1, 2],
539  ["a", "b", "c"],
540  23
541]"#;
542        let headers = Annotated::<Headers>::from_json(json).unwrap();
543        #[derive(Debug, Empty, IntoValue)]
544        pub struct Container {
545            headers: Annotated<Headers>,
546        }
547        assert_eq!(
548            Annotated::new(Container { headers })
549                .to_json_pretty()
550                .unwrap(),
551            r#"{
552  "headers": [
553    [
554      "Accept",
555      "application/json"
556    ],
557    [
558      null,
559      "2"
560    ],
561    null,
562    null
563  ],
564  "_meta": {
565    "headers": {
566      "1": {
567        "0": {
568          "": {
569            "err": [
570              [
571                "invalid_data",
572                {
573                  "reason": "expected a string"
574                }
575              ]
576            ],
577            "val": 1
578          }
579        }
580      },
581      "2": {
582        "": {
583          "err": [
584            [
585              "invalid_data",
586              {
587                "reason": "expected a tuple"
588              }
589            ]
590          ],
591          "val": [
592            "a",
593            "b",
594            "c"
595          ]
596        }
597      },
598      "3": {
599        "": {
600          "err": [
601            [
602              "invalid_data",
603              {
604                "reason": "expected a tuple"
605              }
606            ]
607          ],
608          "val": 23
609        }
610      }
611    }
612  }
613}"#
614        );
615    }
616
617    #[test]
618    fn test_request_roundtrip() {
619        let json = r#"{
620  "url": "https://google.com/search",
621  "method": "GET",
622  "data": {
623    "some": 1
624  },
625  "query_string": [
626    [
627      "q",
628      "foo"
629    ]
630  ],
631  "fragment": "home",
632  "cookies": [
633    [
634      "GOOGLE",
635      "1"
636    ]
637  ],
638  "headers": [
639    [
640      "Referer",
641      "https://google.com/"
642    ]
643  ],
644  "body_size": 1024,
645  "env": {
646    "REMOTE_ADDR": "213.47.147.207"
647  },
648  "inferred_content_type": "application/json",
649  "api_target": "graphql",
650  "other": "value"
651}"#;
652
653        let request = Annotated::new(Request {
654            url: Annotated::new("https://google.com/search".to_string()),
655            method: Annotated::new("GET".to_string()),
656            protocol: Annotated::empty(),
657            data: {
658                let mut map = Object::new();
659                map.insert("some".to_string(), Annotated::new(Value::I64(1)));
660                Annotated::new(Value::Object(map))
661            },
662            query_string: Annotated::new(Query(
663                vec![Annotated::new((
664                    Annotated::new("q".to_string()),
665                    Annotated::new("foo".to_string().into()),
666                ))]
667                .into(),
668            )),
669            fragment: Annotated::new("home".to_string()),
670            cookies: Annotated::new(Cookies({
671                PairList(vec![Annotated::new((
672                    Annotated::new("GOOGLE".to_string()),
673                    Annotated::new("1".to_string()),
674                ))])
675            })),
676            headers: Annotated::new(Headers({
677                let headers = vec![Annotated::new((
678                    Annotated::new("Referer".to_string().into()),
679                    Annotated::new("https://google.com/".to_string().into()),
680                ))];
681                PairList(headers)
682            })),
683            body_size: Annotated::new(1024),
684            env: Annotated::new({
685                let mut map = Object::new();
686                map.insert(
687                    "REMOTE_ADDR".to_string(),
688                    Annotated::new(Value::String("213.47.147.207".to_string())),
689                );
690                map
691            }),
692            inferred_content_type: Annotated::new("application/json".to_string()),
693            api_target: Annotated::new("graphql".to_string()),
694            other: {
695                let mut map = Object::new();
696                map.insert(
697                    "other".to_string(),
698                    Annotated::new(Value::String("value".to_string())),
699                );
700                map
701            },
702        });
703
704        assert_eq!(request, Annotated::from_json(json).unwrap());
705        assert_eq!(json, request.to_json_pretty().unwrap());
706    }
707
708    #[test]
709    fn test_query_string() {
710        let query = Annotated::new(Query(
711            vec![Annotated::new((
712                Annotated::new("foo".to_string()),
713                Annotated::new("bar".to_string().into()),
714            ))]
715            .into(),
716        ));
717        assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
718        assert_eq!(query, Annotated::from_json("\"?foo=bar\"").unwrap());
719
720        let query = Annotated::new(Query(
721            vec![
722                Annotated::new((
723                    Annotated::new("foo".to_string()),
724                    Annotated::new("bar".to_string().into()),
725                )),
726                Annotated::new((
727                    Annotated::new("baz".to_string()),
728                    Annotated::new("42".to_string().into()),
729                )),
730            ]
731            .into(),
732        ));
733        assert_eq!(query, Annotated::from_json("\"foo=bar&baz=42\"").unwrap());
734    }
735
736    #[test]
737    fn test_query_string_legacy_nested() {
738        // this test covers a case that previously was let through the ingest system but in a bad
739        // way.  This was untyped and became a str repr() in Python.  New SDKs will no longer send
740        // nested objects here but for legacy values we instead serialize it out as JSON.
741        let query = Annotated::new(Query(
742            vec![Annotated::new((
743                Annotated::new("foo".to_string()),
744                Annotated::new("bar".to_string().into()),
745            ))]
746            .into(),
747        ));
748        assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
749
750        let query = Annotated::new(Query(
751            vec![
752                Annotated::new((
753                    Annotated::new("baz".to_string()),
754                    Annotated::new(r#"{"a":42}"#.to_string().into()),
755                )),
756                Annotated::new((
757                    Annotated::new("foo".to_string()),
758                    Annotated::new("bar".to_string().into()),
759                )),
760            ]
761            .into(),
762        ));
763        assert_eq!(
764            query,
765            Annotated::from_json(
766                r#"
767        {
768            "foo": "bar",
769            "baz": {"a": 42}
770        }
771    "#
772            )
773            .unwrap()
774        );
775    }
776
777    #[test]
778    fn test_query_invalid() {
779        let query = Annotated::<Query>::from_error(
780            Error::expected("a query string or map"),
781            Some(Value::I64(42)),
782        );
783        assert_eq!(query, Annotated::from_json("42").unwrap());
784    }
785
786    #[test]
787    fn test_cookies_parsing() {
788        let json = "\" PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;\"";
789
790        let map = vec![
791            Annotated::new((
792                Annotated::new("PHPSESSID".to_string()),
793                Annotated::new("298zf09hf012fh2".to_string()),
794            )),
795            Annotated::new((
796                Annotated::new("csrftoken".to_string()),
797                Annotated::new("u32t4o3tb3gg43".to_string()),
798            )),
799            Annotated::new((
800                Annotated::new("_gat".to_string()),
801                Annotated::new("1".to_string()),
802            )),
803        ];
804
805        let cookies = Annotated::new(Cookies(PairList(map)));
806        assert_eq!(cookies, Annotated::from_json(json).unwrap());
807    }
808
809    #[test]
810    fn test_cookies_array() {
811        let input = r#"{"cookies":[["foo","bar"],["invalid", 42],["none",null]]}"#;
812        let output = r#"{"cookies":[["foo","bar"],["invalid",null],["none",null]],"_meta":{"cookies":{"1":{"1":{"":{"err":[["invalid_data",{"reason":"expected a string"}]],"val":42}}}}}}"#;
813
814        let map = vec![
815            Annotated::new((
816                Annotated::new("foo".to_string()),
817                Annotated::new("bar".to_string()),
818            )),
819            Annotated::new((
820                Annotated::new("invalid".to_string()),
821                Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
822            )),
823            Annotated::new((Annotated::new("none".to_string()), Annotated::empty())),
824        ];
825
826        let cookies = Annotated::new(Cookies(PairList(map)));
827        let request = Annotated::new(Request {
828            cookies,
829            ..Default::default()
830        });
831        assert_eq!(request, Annotated::from_json(input).unwrap());
832        assert_eq!(request.to_json().unwrap(), output);
833    }
834
835    #[test]
836    fn test_cookies_object() {
837        let json = r#"{"foo":"bar", "invalid": 42}"#;
838
839        let map = vec![
840            Annotated::new((
841                Annotated::new("foo".to_string()),
842                Annotated::new("bar".to_string()),
843            )),
844            Annotated::new((
845                Annotated::new("invalid".to_string()),
846                Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
847            )),
848        ];
849
850        let cookies = Annotated::new(Cookies(PairList(map)));
851        assert_eq!(cookies, Annotated::from_json(json).unwrap());
852    }
853
854    #[test]
855    fn test_cookies_invalid() {
856        let cookies =
857            Annotated::<Cookies>::from_error(Error::expected("cookies"), Some(Value::I64(42)));
858        assert_eq!(cookies, Annotated::from_json("42").unwrap());
859    }
860
861    #[test]
862    fn test_querystring_without_value() {
863        let json = r#""foo=bar&baz""#;
864
865        let query = Annotated::new(Query(
866            vec![
867                Annotated::new((
868                    Annotated::new("foo".to_string()),
869                    Annotated::new("bar".to_string().into()),
870                )),
871                Annotated::new((
872                    Annotated::new("baz".to_string()),
873                    Annotated::new("".to_string().into()),
874                )),
875            ]
876            .into(),
877        ));
878
879        assert_eq!(query, Annotated::from_json(json).unwrap());
880    }
881
882    #[test]
883    fn test_headers_lenient_value() {
884        let input = r#"{
885  "headers": {
886    "X-Foo": "",
887    "X-Bar": 42
888  }
889}"#;
890
891        let output = r#"{
892  "headers": [
893    [
894      "X-Bar",
895      "42"
896    ],
897    [
898      "X-Foo",
899      ""
900    ]
901  ]
902}"#;
903
904        let request = Annotated::new(Request {
905            headers: Annotated::new(Headers(PairList(vec![
906                Annotated::new((
907                    Annotated::new("X-Bar".to_string().into()),
908                    Annotated::new("42".to_string().into()),
909                )),
910                Annotated::new((
911                    Annotated::new("X-Foo".to_string().into()),
912                    Annotated::new("".to_string().into()),
913                )),
914            ]))),
915            ..Default::default()
916        });
917
918        assert_eq!(Annotated::from_json(input).unwrap(), request);
919        assert_eq!(request.to_json_pretty().unwrap(), output);
920    }
921
922    #[test]
923    fn test_headers_multiple_values() {
924        let input = r#"{
925  "headers": {
926    "X-Foo": [""],
927    "X-Bar": [
928      42,
929      "bar",
930      "baz"
931    ]
932  }
933}"#;
934
935        let output = r#"{
936  "headers": [
937    [
938      "X-Bar",
939      "42,bar,baz"
940    ],
941    [
942      "X-Foo",
943      ""
944    ]
945  ]
946}"#;
947
948        let request = Annotated::new(Request {
949            headers: Annotated::new(Headers(PairList(vec![
950                Annotated::new((
951                    Annotated::new("X-Bar".to_string().into()),
952                    Annotated::new("42,bar,baz".to_string().into()),
953                )),
954                Annotated::new((
955                    Annotated::new("X-Foo".to_string().into()),
956                    Annotated::new("".to_string().into()),
957                )),
958            ]))),
959            ..Default::default()
960        });
961
962        assert_eq!(Annotated::from_json(input).unwrap(), request);
963        assert_eq!(request.to_json_pretty().unwrap(), output);
964    }
965}