proptest_http_message/request_line/target/components/
query.rs

1//! URL query strategies.
2
3use std::{ops::RangeInclusive, sync::LazyLock};
4
5use array_concat::{concat_arrays, concat_arrays_size};
6use proptest::prelude::Strategy;
7
8use crate::request_line::target::components::{
9  UNRESERVED, UrlChar, char_diff_intervals, safe_and_percent_encoded_char, url_chars_to_string,
10};
11
12static QUERY_UNSAFE_CHARS: LazyLock<Vec<RangeInclusive<char>>> =
13  LazyLock::new(|| char_diff_intervals(&QUERY_SAFE_CHARS));
14
15// space character is included in safe chars, because it should be replaced with
16// '+' and not percent-encoded.
17// even if RFC 3986 states that sub-delims can be part of a query, it also states
18// that all reserved characters should be percent encoded. the safe option is to
19// percent encode all reserved characters.
20const QUERY_SAFE_CHARS: [char; concat_arrays_size!(UNRESERVED) + 5] =
21  concat_arrays!(UNRESERVED, [':', '@', '/', '?', ' ']);
22
23fn chars() -> impl Strategy<Value = UrlChar> {
24  safe_and_percent_encoded_char(&QUERY_SAFE_CHARS, &QUERY_UNSAFE_CHARS).prop_map(|c| {
25    if let UrlChar::Normal(c) = c
26      && c == ' '
27    {
28      // url-encoding requires space to be encoded as '+' instead of percent encoding
29      UrlChar::Normal('+')
30    } else {
31      c
32    }
33  })
34}
35
36fn query_subcomponent(min_chars: usize, max_chars: usize) -> impl Strategy<Value = String> {
37  proptest::collection::vec(chars(), min_chars..=max_chars).prop_map(url_chars_to_string)
38}
39
40/// URL Query parameter.
41#[derive(Debug)]
42pub struct QueryParam {
43  /// param key.
44  pub key: String,
45  /// param value.
46  pub value: Option<String>,
47}
48
49/// single URL query param
50/// # Returns
51/// [`QueryParam`] with it representation in the form `<key>=<value>`.
52pub fn query_param() -> impl Strategy<Value = (QueryParam, String)> {
53  (query_subcomponent(0, 50), query_subcomponent(0, 50)).prop_map(|(key, value)| {
54    let repr = format!("{key}={value}");
55    (QueryParam { key, value: if value.is_empty() { None } else { Some(value) } }, repr)
56  })
57}
58
59/// URL query.
60///
61/// # Returns
62/// Vec of [`QueryParam`] and it representation. individual params are separated by `'&'`.
63pub fn query(
64  min_queries: usize,
65  max_queries: usize,
66) -> impl Strategy<Value = (Vec<QueryParam>, String)> {
67  proptest::collection::vec(query_param(), min_queries..=max_queries).prop_map(|params| {
68    let (params, reprs): (Vec<_>, Vec<_>) = params.into_iter().unzip();
69    (params, reprs.join("&"))
70  })
71}
72
73#[cfg(test)]
74mod tests {
75  use proptest::proptest;
76
77  use super::*;
78
79  proptest! {
80    #[test]
81    fn query_param_works((param, repr) in query_param()) {
82      println!("{repr:?}");
83      assert!(repr.starts_with(param.key.as_str()), "param should start with key but got {param:?} {repr:?}");
84      assert!(repr.ends_with(param.value.as_deref().unwrap_or_default()), "param should end with value but got {param:?} {repr:?}");
85    }
86  }
87}