querylizer/
lib.rs

1// Copyright 2022 Jonathan Giddy
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16
17use serde::ser;
18use thiserror::Error;
19
20pub use deep::DeepObject;
21pub use deepform::DeepForm;
22pub use form::Form;
23pub use simple::Simple;
24
25#[derive(Error, PartialEq, Debug)]
26pub enum QuerylizerError {
27    #[error("serialization error")]
28    SerializationError(String),
29    #[error("nested containers not supported")]
30    UnsupportedNesting,
31    #[error("unsupported value")]
32    UnsupportedValue,
33    #[error("unknown error")]
34    Unknown,
35}
36
37impl ser::Error for QuerylizerError {
38    fn custom<T>(msg: T) -> Self
39    where
40        T: Display,
41    {
42        QuerylizerError::SerializationError(msg.to_string())
43    }
44}
45
46// See https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
47
48const UNRESERVED: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
49    .remove(b'-')
50    .remove(b'.')
51    .remove(b'_')
52    .remove(b'~');
53
54const PATH_SIMPLE: &percent_encoding::AsciiSet = &UNRESERVED
55    .remove(b'!')
56    .remove(b'$')
57    .remove(b'&')
58    .remove(b'\'')
59    .remove(b'(')
60    .remove(b')')
61    .remove(b'*')
62    .remove(b'+')
63    .remove(b',')
64    .remove(b';')
65    .remove(b'=')
66    .remove(b':')
67    .remove(b'@');
68
69const QUERY_SIMPLE: &percent_encoding::AsciiSet = UNRESERVED;
70
71const QUERY_SIMPLE_ALLOW_RESERVED: &percent_encoding::AsciiSet = &QUERY_SIMPLE
72    .remove(b':')
73    .remove(b'/')
74    .remove(b'?')
75    .remove(b'#')
76    .remove(b'[')
77    .remove(b']')
78    .remove(b'@')
79    .remove(b'!')
80    .remove(b'$')
81    .remove(b'&')
82    .remove(b'\'')
83    .remove(b'(')
84    .remove(b')')
85    .remove(b'*')
86    .remove(b'+')
87    .remove(b',')
88    .remove(b';')
89    .remove(b'=');
90
91// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
92const WWW_FORM_URL_ENCODED: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
93    .remove(b'*')
94    .remove(b'-')
95    .remove(b'.')
96    .remove(b'_');
97
98/// Encode a string to allow it to be added to a URL path.
99pub fn encode_path(s: &str) -> impl Iterator<Item = &str> {
100    percent_encoding::utf8_percent_encode(s, PATH_SIMPLE)
101}
102
103/// Encode a string to allow it to be added to a URL query.
104///
105/// # Example
106///
107/// ```
108/// use querylizer::{encode_query, Form};
109/// #[derive(serde::Serialize)]
110/// struct V {
111///     a: &'static str,
112///     b: &'static str,
113/// }
114/// let v = V { a: "a red&car~", b: "a/blue=boat" };
115/// let s = Form::to_string("", &v, true, &encode_query).unwrap();
116/// assert_eq!(s, "a=a%20red%26car~&b=a%2Fblue%3Dboat");
117/// ```
118pub fn encode_query(s: &str) -> impl Iterator<Item = &str> {
119    percent_encoding::utf8_percent_encode(s, QUERY_SIMPLE)
120}
121
122/// Encode a string to allow it to be added to a URL query, but allowing reserved
123/// characters to pass unencoded.
124///
125/// Since this allows `&` and `#` to appear in the query value, it should only be used when the URL
126/// query contains a single parameter.
127///
128/// # Example
129///
130/// ```
131/// use querylizer::{encode_query_allow_reserved, Form};
132/// #[derive(serde::Serialize)]
133/// struct V {
134///     a: &'static str,
135///     b: &'static str,
136/// }
137/// let v = V { a: "a red&car~", b: "a/blue=boat" };
138/// let s = Form::to_string("", &v, true, &encode_query_allow_reserved).unwrap();
139/// assert_eq!(s, "a=a%20red&car~&b=a/blue=boat");
140/// ```
141pub fn encode_query_allow_reserved(s: &str) -> impl Iterator<Item = &str> {
142    percent_encoding::utf8_percent_encode(s, QUERY_SIMPLE_ALLOW_RESERVED)
143}
144
145/// Encode a string to allow it to be added to an `application/x-www-form-urlencoded` form.
146///
147/// To create a form body, use `Form` with `explode=true` and pass a structure with fields of type
148/// boolean, numeric, or string.  The `name` parameter is ignored so its value is not significant.
149///
150/// # Example
151///
152/// ```
153/// use querylizer::{encode_www_form_urlencoded, Form};
154/// #[derive(serde::Serialize)]
155/// struct V {
156///     a: &'static str,
157///     b: &'static str,
158/// }
159/// let v = V { a: "a red&car~", b: "a/blue=boat" };
160/// let s = Form::to_string("", &v, true, &encode_www_form_urlencoded).unwrap();
161/// assert_eq!(s, "a=a%20red%26car%7E&b=a%2Fblue%3Dboat");
162/// ```
163pub fn encode_www_form_urlencoded(s: &str) -> impl Iterator<Item = &str> {
164    percent_encoding::utf8_percent_encode(s, WWW_FORM_URL_ENCODED)
165}
166
167/// An identity function that does not encode any characters.
168///
169/// This can be passed to the `querylizer` serializers if no encoding should be done.
170pub fn passthrough(s: &str) -> impl Iterator<Item = &str> {
171    ::std::iter::once(s)
172}
173
174// Use a trait to represent `Fn(&str) -> impl Iterator<Item=&str>`, to allow it to
175// be stored in a struct. Derived from https://stackoverflow.com/a/63558160/2644842
176pub trait EncodingFn<'a> {
177    type Iter: Iterator<Item = &'a str>;
178    fn call(&self, arg: &'a str) -> Self::Iter;
179}
180
181impl<'a, I, F> EncodingFn<'a> for F
182where
183    F: Fn(&'a str) -> I,
184    I: Iterator<Item = &'a str>,
185{
186    type Iter = I;
187    fn call(&self, s: &'a str) -> I {
188        self(s)
189    }
190}
191
192mod deep;
193mod deepform;
194mod form;
195mod simple;