to_query_params/
lib.rs

1//! `to-query-params` exports the [`QueryParams`] derive macro for public consumption, and the
2//! [`ToQueryParams`] trait that it derives.
3#[doc(inline)]
4pub use query_params_macro::QueryParams;
5
6#[doc(hidden)]
7pub use urlencoding;
8
9extern crate self as to_query_params;
10
11/// [`ToQueryParams`] contains two methods, `to_query_params` and `to_encoded_params`, which each
12/// produce a `Vec<(String, String)>` representing the struct as query parameters, either un-encoded
13/// or url-encoded respectively.
14///
15pub trait ToQueryParams {
16    /// Creates a `Vec<(String, String)>` as the un-encoded (key, value) pairs for query parameters.
17    fn to_query_params(&self) -> Vec<(String, String)>;
18
19    /// Creates a `Vec<(String, String)>` as the url-encoded (key, value) pairs for query parameters.
20    fn to_encoded_params(&self) -> Vec<(String, String)> {
21        self.to_query_params()
22            .iter()
23            .map(|(k, v)| {
24                (
25                    urlencoding::encode(k).to_string(),
26                    urlencoding::encode(v).to_string(),
27                )
28            })
29            .collect()
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[derive(QueryParams, Debug, PartialEq)]
38    struct TestItem {
39        #[query(required)]
40        a: i32,
41        #[query(required)]
42        b: i32,
43    }
44
45    #[derive(QueryParams, Debug, PartialEq)]
46    struct TestExcludeItem {
47        #[query(required)]
48        a: i32,
49        #[query(exclude)]
50        b: i32,
51        #[query(exclude)]
52        c: Option<i32>,
53        #[query(required)]
54        d: i32,
55    }
56
57    #[derive(QueryParams, Debug, PartialEq)]
58    struct TestStringItem {
59        #[query(required)]
60        a: String,
61        #[query(required, rename = "please encode")]
62        b: String,
63    }
64
65    #[derive(QueryParams, Debug, PartialEq)]
66    struct TestItemRequiredRename {
67        #[query(required, rename = "alpha")]
68        a: i32,
69        #[query(required)]
70        b: i32,
71    }
72
73    #[derive(QueryParams, Debug, PartialEq)]
74    struct TestItemOptionals {
75        a: Option<String>,
76        b: Option<bool>,
77    }
78
79    #[derive(QueryParams, Debug, PartialEq)]
80    struct TestItemRename {
81        #[query(rename = "alpha")]
82        a: Option<String>,
83        #[query(rename = "beta")]
84        b: Option<bool>,
85    }
86
87    #[derive(QueryParams, Debug, PartialEq)]
88    struct TestItemMixedRequiredOptionals {
89        a: Option<String>,
90        b: Option<bool>,
91        #[query(required)]
92        c: i32,
93    }
94
95    #[derive(QueryParams, Debug, PartialEq)]
96    struct TestItemMixedRequiredOptionalsAndRename {
97        #[query(rename = "alpha")]
98        a: Option<String>,
99        b: Option<bool>,
100        #[query(rename = "gamma")]
101        #[query(required)]
102        c: i32,
103    }
104
105    #[test]
106    fn test_developer_experience() {
107        let t = trybuild::TestCases::new();
108        t.compile_fail("tests/ui/*.rs");
109    }
110
111    #[test]
112    fn test_query_params_required_case() {
113        let test_item = TestItem { a: 0, b: 1 };
114
115        let expected = vec![
116            ("a".to_string(), "0".to_string()),
117            ("b".to_string(), "1".to_string()),
118        ];
119
120        assert_eq!(test_item.to_query_params(), expected);
121    }
122
123    #[test]
124    fn test_exclude_attribute() {
125        let test_item = TestExcludeItem {
126            a: 0,
127            b: 1,
128            c: Some(2),
129            d: 3,
130        };
131
132        let expected = vec![
133            ("a".to_string(), "0".to_string()),
134            ("d".to_string(), "3".to_string()),
135        ];
136
137        assert_eq!(test_item.to_query_params(), expected);
138    }
139
140    #[test]
141    fn test_query_params_encoding() {
142        let test_item = TestStringItem {
143            a: "please encode me".into(),
144            b: "this works?".into(),
145        };
146
147        let expected = vec![
148            ("a".to_string(), "please%20encode%20me".to_string()),
149            ("please%20encode".to_string(), "this%20works%3F".to_string()),
150        ];
151
152        assert_eq!(test_item.to_encoded_params(), expected);
153    }
154
155    #[test]
156    fn test_required_rename_case() {
157        let test_item = TestItemRequiredRename { a: 0, b: 1 };
158
159        let expected = vec![
160            ("alpha".to_string(), "0".to_string()),
161            ("b".to_string(), "1".to_string()),
162        ];
163
164        assert_eq!(test_item.to_query_params(), expected);
165    }
166
167    #[test]
168    fn test_query_params_optional_case() {
169        let test_item = TestItemOptionals {
170            a: Some("a".to_string()),
171            b: None,
172        };
173
174        let expected = vec![("a".to_string(), "a".to_string())];
175
176        assert_eq!(test_item.to_query_params(), expected);
177    }
178
179    #[test]
180    fn test_query_params_optional_rename_case() {
181        let test_item = TestItemRename {
182            a: Some("a".to_string()),
183            b: Some(true),
184        };
185
186        let expected = vec![
187            ("alpha".to_string(), "a".to_string()),
188            ("beta".to_string(), "true".to_string()),
189        ];
190
191        assert_eq!(test_item.to_query_params(), expected);
192    }
193
194    #[test]
195    fn test_query_params_mixed_case() {
196        let test_item = TestItemMixedRequiredOptionals {
197            a: Some("a".to_string()),
198            b: None,
199            c: 42,
200        };
201
202        let expected = vec![
203            ("a".to_string(), "a".to_string()),
204            ("c".to_string(), "42".to_string()),
205        ];
206
207        let mut actual = test_item.to_query_params();
208
209        // order is not guaranteed due to splitting up optional and required params
210        actual.sort();
211
212        assert_eq!(actual, expected);
213    }
214
215    #[test]
216    fn test_query_params_mixed_case_with_rename() {
217        let test_item = TestItemMixedRequiredOptionalsAndRename {
218            a: Some("a".to_string()),
219            b: None,
220            c: 42,
221        };
222
223        let expected = vec![
224            ("alpha".to_string(), "a".to_string()),
225            ("gamma".to_string(), "42".to_string()),
226        ];
227
228        let mut actual = test_item.to_query_params();
229
230        // order is not guaranteed due to splitting up optional and required params
231        actual.sort();
232
233        assert_eq!(actual, expected);
234    }
235}