Skip to main content

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_owned().into(),
31                cookie.value().to_owned().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((k, v)) = item.value()
233                && k.as_str() == Some(key)
234            {
235                return v.as_str();
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    /// Serializes the query pairs back to a URL-encoded query string without a leading `?`.
291    ///
292    /// Returns `None` if there are no valid pairs.
293    pub fn to_query_string(&self) -> Option<String> {
294        let mut serializer = form_urlencoded::Serializer::new(String::new());
295        let mut has_pairs = false;
296        for pair in self.iter() {
297            if let Some((key, value)) = pair.value()
298                && let (Some(k), Some(v)) = (key.as_str(), value.as_str())
299            {
300                serializer.append_pair(k, v);
301                has_pairs = true;
302            }
303        }
304        has_pairs.then(|| serializer.finish())
305    }
306}
307
308impl std::ops::Deref for Query {
309    type Target = PairList<(Annotated<String>, Annotated<JsonLenientString>)>;
310
311    fn deref(&self) -> &Self::Target {
312        &self.0
313    }
314}
315
316impl std::ops::DerefMut for Query {
317    fn deref_mut(&mut self) -> &mut Self::Target {
318        &mut self.0
319    }
320}
321
322impl<K, V> FromIterator<(K, V)> for Query
323where
324    K: Into<String>,
325    V: Into<String>,
326{
327    fn from_iter<T>(iter: T) -> Self
328    where
329        T: IntoIterator<Item = (K, V)>,
330    {
331        let pairs = iter.into_iter().map(|(key, value)| {
332            Annotated::new((
333                Annotated::new(key.into()),
334                Annotated::new(value.into().into()),
335            ))
336        });
337
338        Query(pairs.collect())
339    }
340}
341
342impl FromValue for Query {
343    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
344        match value {
345            Annotated(Some(Value::String(v)), meta) => Annotated(Some(Query::parse(&v)), meta),
346            annotated @ Annotated(Some(Value::Object(_)), _)
347            | annotated @ Annotated(Some(Value::Array(_)), _) => {
348                PairList::from_value(annotated).map_value(Query)
349            }
350            Annotated(None, meta) => Annotated(None, meta),
351            Annotated(Some(value), mut meta) => {
352                meta.add_error(Error::expected("a query string or map"));
353                meta.set_original_value(Some(value));
354                Annotated(None, meta)
355            }
356        }
357    }
358}
359
360/// Http request information.
361///
362/// The Request interface contains information on a HTTP request related to the event. In client
363/// SDKs, this can be an outgoing request, or the request that rendered the current web page. On
364/// server SDKs, this could be the incoming web request that is being handled.
365///
366/// The data variable should only contain the request body (not the query string). It can either be
367/// a dictionary (for standard HTTP requests) or a raw request body.
368///
369/// ### Ordered Maps
370///
371/// In the Request interface, several attributes can either be declared as string, object, or list
372/// of tuples. Sentry attempts to parse structured information from the string representation in
373/// such cases.
374///
375/// Sometimes, keys can be declared multiple times, or the order of elements matters. In such
376/// cases, use the tuple representation over a plain object.
377///
378/// Example of request headers as object:
379///
380/// ```json
381/// {
382///   "content-type": "application/json",
383///   "accept": "application/json, application/xml"
384/// }
385/// ```
386///
387/// Example of the same headers as list of tuples:
388///
389/// ```json
390/// [
391///   ["content-type", "application/json"],
392///   ["accept", "application/json"],
393///   ["accept", "application/xml"]
394/// ]
395/// ```
396///
397/// Example of a fully populated request object:
398///
399/// ```json
400/// {
401///   "request": {
402///     "method": "POST",
403///     "url": "http://absolute.uri/foo",
404///     "query_string": "query=foobar&page=2",
405///     "data": {
406///       "foo": "bar"
407///     },
408///     "cookies": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
409///     "headers": {
410///       "content-type": "text/html"
411///     },
412///     "env": {
413///       "REMOTE_ADDR": "192.168.0.1"
414///     }
415///   }
416/// }
417/// ```
418#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
419#[metastructure(process_func = "process_request", value_type = "Request")]
420pub struct Request {
421    /// The URL of the request if available.
422    ///
423    ///The query string can be declared either as part of the `url`, or separately in `query_string`.
424    #[metastructure(max_chars = 256, max_chars_allowance = 40, pii = "maybe")]
425    pub url: Annotated<String>,
426
427    /// HTTP request method.
428    pub method: Annotated<String>,
429
430    /// HTTP protocol.
431    pub protocol: Annotated<String>,
432
433    /// Request data in any format that makes sense.
434    ///
435    /// SDKs should discard large and binary bodies by default. Can be given as a string or
436    /// structural data of any format.
437    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
438    pub data: Annotated<Value>,
439
440    /// The query string component of the URL.
441    ///
442    /// Can be given as unparsed string, dictionary, or list of tuples.
443    ///
444    /// If the query string is not declared and part of the `url`, Sentry moves it to the
445    /// query string.
446    #[metastructure(pii = "true", max_depth = 3, max_bytes = 1024)]
447    #[metastructure(skip_serialization = "empty")]
448    pub query_string: Annotated<Query>,
449
450    /// The fragment of the request URI.
451    #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
452    #[metastructure(skip_serialization = "empty")]
453    pub fragment: Annotated<String>,
454
455    /// The cookie values.
456    ///
457    /// Can be given unparsed as string, as dictionary, or as a list of tuples.
458    #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
459    #[metastructure(skip_serialization = "empty")]
460    pub cookies: Annotated<Cookies>,
461
462    /// A dictionary of submitted headers.
463    ///
464    /// If a header appears multiple times it, needs to be merged according to the HTTP standard
465    /// for header merging. Header names are treated case-insensitively by Sentry.
466    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
467    #[metastructure(skip_serialization = "empty")]
468    pub headers: Annotated<Headers>,
469
470    /// HTTP request body size.
471    pub body_size: Annotated<u64>,
472
473    /// Server environment data, such as CGI/WSGI.
474    ///
475    /// A dictionary containing environment information passed from the server. This is where
476    /// information such as CGI/WSGI/Rack keys go that are not HTTP headers.
477    ///
478    /// Sentry will explicitly look for `REMOTE_ADDR` to extract an IP address.
479    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
480    #[metastructure(skip_serialization = "empty")]
481    pub env: Annotated<Object<Value>>,
482
483    /// The inferred content type of the request payload.
484    #[metastructure(skip_serialization = "empty")]
485    pub inferred_content_type: Annotated<String>,
486
487    /// The API target/specification that made the request.
488    ///
489    /// Values can be `graphql`, `rest`, etc.
490    ///
491    /// The data field should contain the request and response bodies based on its target specification.
492    ///
493    /// This information can be used for better data scrubbing and normalization.
494    pub api_target: Annotated<String>,
495
496    /// Additional arbitrary fields for forwards compatibility.
497    #[metastructure(additional_properties, pii = "true")]
498    pub other: Object<Value>,
499}
500
501#[cfg(test)]
502mod tests {
503    use similar_asserts::assert_eq;
504
505    use super::*;
506
507    #[test]
508    fn test_header_normalization() {
509        let json = r#"{
510  "-other-": "header",
511  "accept": "application/json",
512  "WWW-Authenticate": "basic",
513  "x-sentry": "version=8"
514}"#;
515
516        let headers = vec![
517            Annotated::new((
518                Annotated::new("-Other-".to_owned().into()),
519                Annotated::new("header".to_owned().into()),
520            )),
521            Annotated::new((
522                Annotated::new("Accept".to_owned().into()),
523                Annotated::new("application/json".to_owned().into()),
524            )),
525            Annotated::new((
526                Annotated::new("WWW-Authenticate".to_owned().into()),
527                Annotated::new("basic".to_owned().into()),
528            )),
529            Annotated::new((
530                Annotated::new("X-Sentry".to_owned().into()),
531                Annotated::new("version=8".to_owned().into()),
532            )),
533        ];
534
535        let headers = Annotated::new(Headers(PairList(headers)));
536        assert_eq!(headers, Annotated::from_json(json).unwrap());
537    }
538
539    #[test]
540    fn test_header_from_sequence() {
541        let json = r#"[
542  ["accept", "application/json"]
543]"#;
544
545        let headers = vec![Annotated::new((
546            Annotated::new("Accept".to_owned().into()),
547            Annotated::new("application/json".to_owned().into()),
548        ))];
549
550        let headers = Annotated::new(Headers(PairList(headers)));
551        assert_eq!(headers, Annotated::from_json(json).unwrap());
552
553        let json = r#"[
554  ["accept", "application/json"],
555  [1, 2],
556  ["a", "b", "c"],
557  23
558]"#;
559        let headers = Annotated::<Headers>::from_json(json).unwrap();
560        #[derive(Debug, Empty, IntoValue)]
561        pub struct Container {
562            headers: Annotated<Headers>,
563        }
564        assert_eq!(
565            Annotated::new(Container { headers })
566                .to_json_pretty()
567                .unwrap(),
568            r#"{
569  "headers": [
570    [
571      "Accept",
572      "application/json"
573    ],
574    [
575      null,
576      "2"
577    ],
578    null,
579    null
580  ],
581  "_meta": {
582    "headers": {
583      "1": {
584        "0": {
585          "": {
586            "err": [
587              [
588                "invalid_data",
589                {
590                  "reason": "expected a string"
591                }
592              ]
593            ],
594            "val": 1
595          }
596        }
597      },
598      "2": {
599        "": {
600          "err": [
601            [
602              "invalid_data",
603              {
604                "reason": "expected a tuple"
605              }
606            ]
607          ],
608          "val": [
609            "a",
610            "b",
611            "c"
612          ]
613        }
614      },
615      "3": {
616        "": {
617          "err": [
618            [
619              "invalid_data",
620              {
621                "reason": "expected a tuple"
622              }
623            ]
624          ],
625          "val": 23
626        }
627      }
628    }
629  }
630}"#
631        );
632    }
633
634    #[test]
635    fn test_request_roundtrip() {
636        let json = r#"{
637  "url": "https://google.com/search",
638  "method": "GET",
639  "data": {
640    "some": 1
641  },
642  "query_string": [
643    [
644      "q",
645      "foo"
646    ]
647  ],
648  "fragment": "home",
649  "cookies": [
650    [
651      "GOOGLE",
652      "1"
653    ]
654  ],
655  "headers": [
656    [
657      "Referer",
658      "https://google.com/"
659    ]
660  ],
661  "body_size": 1024,
662  "env": {
663    "REMOTE_ADDR": "213.47.147.207"
664  },
665  "inferred_content_type": "application/json",
666  "api_target": "graphql",
667  "other": "value"
668}"#;
669
670        let request = Annotated::new(Request {
671            url: Annotated::new("https://google.com/search".to_owned()),
672            method: Annotated::new("GET".to_owned()),
673            protocol: Annotated::empty(),
674            data: {
675                let mut map = Object::new();
676                map.insert("some".to_owned(), Annotated::new(Value::I64(1)));
677                Annotated::new(Value::Object(map))
678            },
679            query_string: Annotated::new(Query(
680                vec![Annotated::new((
681                    Annotated::new("q".to_owned()),
682                    Annotated::new("foo".to_owned().into()),
683                ))]
684                .into(),
685            )),
686            fragment: Annotated::new("home".to_owned()),
687            cookies: Annotated::new(Cookies({
688                PairList(vec![Annotated::new((
689                    Annotated::new("GOOGLE".to_owned()),
690                    Annotated::new("1".to_owned()),
691                ))])
692            })),
693            headers: Annotated::new(Headers({
694                let headers = vec![Annotated::new((
695                    Annotated::new("Referer".to_owned().into()),
696                    Annotated::new("https://google.com/".to_owned().into()),
697                ))];
698                PairList(headers)
699            })),
700            body_size: Annotated::new(1024),
701            env: Annotated::new({
702                let mut map = Object::new();
703                map.insert(
704                    "REMOTE_ADDR".to_owned(),
705                    Annotated::new(Value::String("213.47.147.207".to_owned())),
706                );
707                map
708            }),
709            inferred_content_type: Annotated::new("application/json".to_owned()),
710            api_target: Annotated::new("graphql".to_owned()),
711            other: {
712                let mut map = Object::new();
713                map.insert(
714                    "other".to_owned(),
715                    Annotated::new(Value::String("value".to_owned())),
716                );
717                map
718            },
719        });
720
721        assert_eq!(request, Annotated::from_json(json).unwrap());
722        assert_eq!(json, request.to_json_pretty().unwrap());
723    }
724
725    #[test]
726    fn test_query_string() {
727        let query = Annotated::new(Query(
728            vec![Annotated::new((
729                Annotated::new("foo".to_owned()),
730                Annotated::new("bar".to_owned().into()),
731            ))]
732            .into(),
733        ));
734        assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
735        assert_eq!(query, Annotated::from_json("\"?foo=bar\"").unwrap());
736
737        let query = Annotated::new(Query(
738            vec![
739                Annotated::new((
740                    Annotated::new("foo".to_owned()),
741                    Annotated::new("bar".to_owned().into()),
742                )),
743                Annotated::new((
744                    Annotated::new("baz".to_owned()),
745                    Annotated::new("42".to_owned().into()),
746                )),
747            ]
748            .into(),
749        ));
750        assert_eq!(query, Annotated::from_json("\"foo=bar&baz=42\"").unwrap());
751    }
752
753    #[test]
754    fn test_query_string_legacy_nested() {
755        // this test covers a case that previously was let through the ingest system but in a bad
756        // way.  This was untyped and became a str repr() in Python.  New SDKs will no longer send
757        // nested objects here but for legacy values we instead serialize it out as JSON.
758        let query = Annotated::new(Query(
759            vec![Annotated::new((
760                Annotated::new("foo".to_owned()),
761                Annotated::new("bar".to_owned().into()),
762            ))]
763            .into(),
764        ));
765        assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
766
767        let query = Annotated::new(Query(
768            vec![
769                Annotated::new((
770                    Annotated::new("baz".to_owned()),
771                    Annotated::new(r#"{"a":42}"#.to_owned().into()),
772                )),
773                Annotated::new((
774                    Annotated::new("foo".to_owned()),
775                    Annotated::new("bar".to_owned().into()),
776                )),
777            ]
778            .into(),
779        ));
780        assert_eq!(
781            query,
782            Annotated::from_json(
783                r#"
784        {
785            "foo": "bar",
786            "baz": {"a": 42}
787        }
788    "#
789            )
790            .unwrap()
791        );
792    }
793
794    #[test]
795    fn test_query_invalid() {
796        let query = Annotated::<Query>::from_error(
797            Error::expected("a query string or map"),
798            Some(Value::I64(42)),
799        );
800        assert_eq!(query, Annotated::from_json("42").unwrap());
801    }
802
803    #[test]
804    fn test_cookies_parsing() {
805        let json = "\" PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;\"";
806
807        let map = vec![
808            Annotated::new((
809                Annotated::new("PHPSESSID".to_owned()),
810                Annotated::new("298zf09hf012fh2".to_owned()),
811            )),
812            Annotated::new((
813                Annotated::new("csrftoken".to_owned()),
814                Annotated::new("u32t4o3tb3gg43".to_owned()),
815            )),
816            Annotated::new((
817                Annotated::new("_gat".to_owned()),
818                Annotated::new("1".to_owned()),
819            )),
820        ];
821
822        let cookies = Annotated::new(Cookies(PairList(map)));
823        assert_eq!(cookies, Annotated::from_json(json).unwrap());
824    }
825
826    #[test]
827    fn test_cookies_array() {
828        let input = r#"{"cookies":[["foo","bar"],["invalid", 42],["none",null]]}"#;
829        let output = r#"{"cookies":[["foo","bar"],["invalid",null],["none",null]],"_meta":{"cookies":{"1":{"1":{"":{"err":[["invalid_data",{"reason":"expected a string"}]],"val":42}}}}}}"#;
830
831        let map = vec![
832            Annotated::new((
833                Annotated::new("foo".to_owned()),
834                Annotated::new("bar".to_owned()),
835            )),
836            Annotated::new((
837                Annotated::new("invalid".to_owned()),
838                Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
839            )),
840            Annotated::new((Annotated::new("none".to_owned()), Annotated::empty())),
841        ];
842
843        let cookies = Annotated::new(Cookies(PairList(map)));
844        let request = Annotated::new(Request {
845            cookies,
846            ..Default::default()
847        });
848        assert_eq!(request, Annotated::from_json(input).unwrap());
849        assert_eq!(request.to_json().unwrap(), output);
850    }
851
852    #[test]
853    fn test_cookies_object() {
854        let json = r#"{"foo":"bar", "invalid": 42}"#;
855
856        let map = vec![
857            Annotated::new((
858                Annotated::new("foo".to_owned()),
859                Annotated::new("bar".to_owned()),
860            )),
861            Annotated::new((
862                Annotated::new("invalid".to_owned()),
863                Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
864            )),
865        ];
866
867        let cookies = Annotated::new(Cookies(PairList(map)));
868        assert_eq!(cookies, Annotated::from_json(json).unwrap());
869    }
870
871    #[test]
872    fn test_cookies_invalid() {
873        let cookies =
874            Annotated::<Cookies>::from_error(Error::expected("cookies"), Some(Value::I64(42)));
875        assert_eq!(cookies, Annotated::from_json("42").unwrap());
876    }
877
878    #[test]
879    fn test_query_to_query_string() {
880        let query = Query(
881            vec![
882                Annotated::new((
883                    Annotated::new("foo".to_owned()),
884                    Annotated::new("bar".to_owned().into()),
885                )),
886                Annotated::new((
887                    Annotated::new("baz".to_owned()),
888                    Annotated::new("qux".to_owned().into()),
889                )),
890            ]
891            .into(),
892        );
893
894        assert_eq!(query.to_query_string(), Some("foo=bar&baz=qux".to_owned()));
895    }
896
897    #[test]
898    fn test_query_to_query_string_empty() {
899        let query = Query(PairList(vec![]));
900        assert_eq!(query.to_query_string(), None);
901    }
902
903    #[test]
904    fn test_query_to_query_string_special_chars() {
905        let query = Query(
906            vec![Annotated::new((
907                Annotated::new("q".to_owned()),
908                Annotated::new("hello world&more".to_owned().into()),
909            ))]
910            .into(),
911        );
912
913        assert_eq!(
914            query.to_query_string(),
915            Some("q=hello+world%26more".to_owned())
916        );
917    }
918
919    #[test]
920    fn test_querystring_without_value() {
921        let json = r#""foo=bar&baz""#;
922
923        let query = Annotated::new(Query(
924            vec![
925                Annotated::new((
926                    Annotated::new("foo".to_owned()),
927                    Annotated::new("bar".to_owned().into()),
928                )),
929                Annotated::new((
930                    Annotated::new("baz".to_owned()),
931                    Annotated::new("".to_owned().into()),
932                )),
933            ]
934            .into(),
935        ));
936
937        assert_eq!(query, Annotated::from_json(json).unwrap());
938    }
939
940    #[test]
941    fn test_headers_lenient_value() {
942        let input = r#"{
943  "headers": {
944    "X-Foo": "",
945    "X-Bar": 42
946  }
947}"#;
948
949        let output = r#"{
950  "headers": [
951    [
952      "X-Bar",
953      "42"
954    ],
955    [
956      "X-Foo",
957      ""
958    ]
959  ]
960}"#;
961
962        let request = Annotated::new(Request {
963            headers: Annotated::new(Headers(PairList(vec![
964                Annotated::new((
965                    Annotated::new("X-Bar".to_owned().into()),
966                    Annotated::new("42".to_owned().into()),
967                )),
968                Annotated::new((
969                    Annotated::new("X-Foo".to_owned().into()),
970                    Annotated::new("".to_owned().into()),
971                )),
972            ]))),
973            ..Default::default()
974        });
975
976        assert_eq!(Annotated::from_json(input).unwrap(), request);
977        assert_eq!(request.to_json_pretty().unwrap(), output);
978    }
979
980    #[test]
981    fn test_headers_multiple_values() {
982        let input = r#"{
983  "headers": {
984    "X-Foo": [""],
985    "X-Bar": [
986      42,
987      "bar",
988      "baz"
989    ]
990  }
991}"#;
992
993        let output = r#"{
994  "headers": [
995    [
996      "X-Bar",
997      "42,bar,baz"
998    ],
999    [
1000      "X-Foo",
1001      ""
1002    ]
1003  ]
1004}"#;
1005
1006        let request = Annotated::new(Request {
1007            headers: Annotated::new(Headers(PairList(vec![
1008                Annotated::new((
1009                    Annotated::new("X-Bar".to_owned().into()),
1010                    Annotated::new("42,bar,baz".to_owned().into()),
1011                )),
1012                Annotated::new((
1013                    Annotated::new("X-Foo".to_owned().into()),
1014                    Annotated::new("".to_owned().into()),
1015                )),
1016            ]))),
1017            ..Default::default()
1018        });
1019
1020        assert_eq!(Annotated::from_json(input).unwrap(), request);
1021        assert_eq!(request.to_json_pretty().unwrap(), output);
1022    }
1023}