gitlab/api/
params.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7//! Endpoint prelude
8//!
9//! This module re-exports all of the types needed for endpoints to implement the
10//! [`Endpoint`](../trait.Endpoint.html) trait.
11
12use std::borrow::Cow;
13
14use chrono::{DateTime, NaiveDate, Utc};
15use serde_json::Value;
16use url::Url;
17
18use crate::api::BodyError;
19
20/// A trait representing a parameter value.
21pub trait ParamValue<'a> {
22    #[allow(clippy::wrong_self_convention)]
23    /// The parameter value as a string.
24    fn as_value(&self) -> Cow<'a, str>;
25}
26
27impl ParamValue<'static> for bool {
28    fn as_value(&self) -> Cow<'static, str> {
29        if *self {
30            "true".into()
31        } else {
32            "false".into()
33        }
34    }
35}
36
37impl<'a> ParamValue<'a> for &'a str {
38    fn as_value(&self) -> Cow<'a, str> {
39        (*self).into()
40    }
41}
42
43impl ParamValue<'static> for String {
44    fn as_value(&self) -> Cow<'static, str> {
45        self.clone().into()
46    }
47}
48
49impl<'a> ParamValue<'a> for &'a String {
50    fn as_value(&self) -> Cow<'a, str> {
51        (*self).into()
52    }
53}
54
55impl<'a> ParamValue<'a> for Cow<'a, str> {
56    fn as_value(&self) -> Cow<'a, str> {
57        self.clone()
58    }
59}
60
61impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
62    fn as_value(&self) -> Cow<'a, str> {
63        (*self).clone()
64    }
65}
66
67impl ParamValue<'static> for u64 {
68    fn as_value(&self) -> Cow<'static, str> {
69        self.to_string().into()
70    }
71}
72
73impl ParamValue<'static> for f64 {
74    fn as_value(&self) -> Cow<'static, str> {
75        self.to_string().into()
76    }
77}
78
79impl ParamValue<'static> for DateTime<Utc> {
80    fn as_value(&self) -> Cow<'static, str> {
81        self.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
82            .into()
83    }
84}
85
86impl ParamValue<'static> for NaiveDate {
87    fn as_value(&self) -> Cow<'static, str> {
88        format!("{}", self.format("%Y-%m-%d")).into()
89    }
90}
91
92/// A structure for form parameters.
93#[derive(Debug, Default, Clone)]
94pub struct FormParams<'a> {
95    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
96}
97
98impl<'a> FormParams<'a> {
99    /// Push a single parameter.
100    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
101    where
102        K: Into<Cow<'a, str>>,
103        V: ParamValue<'b>,
104        'b: 'a,
105    {
106        self.params.push((key.into(), value.as_value()));
107        self
108    }
109
110    /// Push a single parameter.
111    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
112    where
113        K: Into<Cow<'a, str>>,
114        V: ParamValue<'b>,
115        'b: 'a,
116    {
117        if let Some(value) = value {
118            self.params.push((key.into(), value.as_value()));
119        }
120        self
121    }
122
123    /// Push a set of parameters.
124    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
125    where
126        I: Iterator<Item = (K, V)>,
127        K: Into<Cow<'a, str>>,
128        V: ParamValue<'b>,
129        'b: 'a,
130    {
131        self.params
132            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
133        self
134    }
135
136    /// Encode the parameters into a request body.
137    pub fn into_body(self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
138        let body = serde_urlencoded::to_string(self.params)?;
139        Ok(Some((
140            "application/x-www-form-urlencoded",
141            body.into_bytes(),
142        )))
143    }
144}
145
146/// A structure for JSON parameters.
147#[derive(Debug, Default, Clone)]
148#[non_exhaustive]
149pub struct JsonParams {}
150
151impl JsonParams {
152    /// Clean a JSON value for submission.
153    ///
154    /// Removes `null` and empty array values from top-level objects.
155    pub fn clean(mut val: Value) -> Value {
156        if let Some(obj) = val.as_object_mut() {
157            obj.retain(|_, v| {
158                !v.is_null()
159                    && v.as_array().map(|a| !a.is_empty()).unwrap_or(true)
160                    && v.as_object().map(|o| !o.is_empty()).unwrap_or(true)
161            });
162        }
163
164        val
165    }
166
167    /// Encode the parameters into a request body.
168    pub fn into_body(input: &Value) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
169        let body = serde_json::to_string(input)?;
170        Ok(Some(("application/json", body.into_bytes())))
171    }
172}
173
174/// A structure for query parameters.
175#[derive(Debug, Default, Clone)]
176pub struct QueryParams<'a> {
177    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
178}
179
180impl<'a> QueryParams<'a> {
181    /// Push a single parameter.
182    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
183    where
184        K: Into<Cow<'a, str>>,
185        V: ParamValue<'b>,
186        'b: 'a,
187    {
188        self.params.push((key.into(), value.as_value()));
189        self
190    }
191
192    /// Push a single parameter.
193    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
194    where
195        K: Into<Cow<'a, str>>,
196        V: ParamValue<'b>,
197        'b: 'a,
198    {
199        if let Some(value) = value {
200            self.params.push((key.into(), value.as_value()));
201        }
202        self
203    }
204
205    /// Push a set of parameters.
206    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
207    where
208        I: Iterator<Item = (K, V)>,
209        K: Into<Cow<'a, str>>,
210        V: ParamValue<'b>,
211        'b: 'a,
212    {
213        self.params
214            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
215        self
216    }
217
218    /// Add the parameters to a URL.
219    pub fn add_to_url(&self, url: &mut Url) {
220        let mut pairs = url.query_pairs_mut();
221        pairs.extend_pairs(self.params.iter());
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use serde_json::json;
228
229    use crate::api::{JsonParams, ParamValue};
230
231    #[test]
232    fn bool_str() {
233        let items = &[(true, "true"), (false, "false")];
234
235        for (i, s) in items {
236            assert_eq!((*i).as_value(), *s);
237        }
238    }
239
240    #[test]
241    fn test_str_as_value() {
242        let items = &["foo", "bar"];
243
244        for i in items {
245            assert_eq!(i.as_value(), *i);
246        }
247    }
248
249    #[test]
250    fn test_string_as_value() {
251        let items = &["foo", "bar"];
252
253        for i in items {
254            let s = String::from(*i);
255            assert_eq!(s.as_value(), s);
256        }
257    }
258
259    #[test]
260    fn json_params_clean() {
261        let dirty = json!({
262            "null": null,
263            "int": 1,
264            "str": "str",
265            "array": [null],
266            "empty_array": [],
267            "object": {
268                "nested_null": null,
269                "nested_empty_array": [],
270                "nested_empty_object": {},
271            },
272            "empty_object": {},
273        });
274        let clean = json!({
275            "int": 1,
276            "str": "str",
277            "array": [null],
278            "object": {
279                "nested_null": null,
280                "nested_empty_array": [],
281                "nested_empty_object": {},
282            },
283        });
284
285        assert_eq!(JsonParams::clean(dirty), clean);
286    }
287}