├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qstring" 3 | description = "Query string parser" 4 | version = "0.7.2" 5 | authors = ["Martin Algesten "] 6 | license = "MIT" 7 | repository = "https://github.com/algesten/qstring" 8 | documentation = "https://docs.rs/qstring" 9 | readme = "README.md" 10 | edition = "2018" 11 | keywords = [ "query", "url", "querystring" ] 12 | exclude = [ 13 | ".gitignore", 14 | ] 15 | 16 | [dependencies] 17 | percent-encoding = "2.0" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | qstring 2 | ======= 3 | 4 | Query string parser. Head over to [the docs](https://docs.rs/qstring). 5 | 6 | ### Example 7 | 8 | ```rust 9 | extern crate qstring; 10 | use qstring::QString; 11 | 12 | let qs = QString::from("?foo=bar"); 13 | let val = qs.get("foo").unwrap(); 14 | println!("{}", val); 15 | ``` 16 | 17 | ### MIT License 18 | 19 | Copyright 2017 Martin Algesten 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS}; 4 | use std::iter::Iterator; 5 | 6 | /// A query string. Holds a list of `(key,value)`. 7 | /// 8 | /// Examples 9 | /// 10 | /// Parameters can be get by their names. 11 | /// 12 | /// ``` 13 | /// let qs = qstring::QString::from("?foo=bar%20baz"); 14 | /// let foo = qs.get("foo").unwrap(); 15 | /// assert_eq!(foo, "bar baz"); 16 | /// ``` 17 | /// 18 | /// Parameters not found are `None`. 19 | /// 20 | /// ``` 21 | /// let qs = qstring::QString::from("?foo=bar"); 22 | /// let foo = &qs.get("panda"); 23 | /// assert!(foo.is_none()); 24 | /// ``` 25 | /// 26 | /// The query string can be assembled from pairs. 27 | /// 28 | /// ``` 29 | /// let qs = qstring::QString::new(vec![ 30 | /// ("foo", "bar baz"), 31 | /// ("panda", "true"), 32 | /// ]); 33 | /// assert_eq!(format!("{}", qs), "foo=bar%20baz&panda=true"); 34 | /// ``` 35 | /// 36 | #[derive(Clone, Debug, PartialEq, Default)] 37 | pub struct QString { 38 | pairs: Vec<(String, QValue)>, 39 | } 40 | 41 | #[derive(Clone, Debug, PartialEq)] 42 | pub enum QValue { 43 | Empty, 44 | Value(String), 45 | } 46 | 47 | impl From for QValue { 48 | fn from(s: String) -> QValue { 49 | QValue::Value(s) 50 | } 51 | } 52 | 53 | impl QString { 54 | /// Constructs a `QString` from a list of pairs. 55 | /// 56 | /// ``` 57 | /// let qs = qstring::QString::new(vec![ 58 | /// ("foo", "bar baz"), 59 | /// ("panda", "true"), 60 | /// ]); 61 | /// assert_eq!(format!("{}", qs), "foo=bar%20baz&panda=true"); 62 | /// ``` 63 | pub fn new(params: Vec<(S, T)>) -> QString 64 | where 65 | S: Into, 66 | T: Into, 67 | { 68 | QString { 69 | pairs: params 70 | .into_iter() 71 | .map(|(k, v)| (k.into(), QValue::Value(v.into()))) 72 | .collect(), 73 | } 74 | } 75 | 76 | /// Tells if a query parameter is present. 77 | /// 78 | /// ``` 79 | /// let qs = qstring::QString::from("?foo"); 80 | /// assert!(qs.has("foo")); 81 | /// assert!(qs.get("foo").is_some()); 82 | /// ``` 83 | pub fn has(&self, name: &str) -> bool { 84 | self.pairs.iter().any(|p| p.0 == name) 85 | } 86 | 87 | /// Get a query parameter by name. 88 | /// 89 | /// Empty query parameters (`?foo`) return `""` 90 | /// 91 | /// ``` 92 | /// let qs = qstring::QString::from("?foo=bar"); 93 | /// let foo = qs.get("foo"); 94 | /// assert_eq!(foo, Some("bar")); 95 | /// ``` 96 | pub fn get<'a>(&'a self, name: &str) -> Option<&'a str> { 97 | self.pairs 98 | .iter() 99 | .find(|p| p.0 == name) 100 | .and_then(|p| match p.1 { 101 | QValue::Empty => Some(""), 102 | QValue::Value(ref s) => Some(s), 103 | }) 104 | } 105 | 106 | /// Converts the QString to list of pairs. 107 | /// 108 | /// ``` 109 | /// let qs = qstring::QString::from("?foo=bar&baz=boo"); 110 | /// let ps = qs.into_pairs(); 111 | /// assert_eq!(ps, vec![ 112 | /// ("foo".to_string(), "bar".to_string()), 113 | /// ("baz".to_string(), "boo".to_string()), 114 | /// ]); 115 | /// ``` 116 | pub fn into_pairs(self) -> Vec<(String, String)> { 117 | self.pairs 118 | .into_iter() 119 | .map(|p| { 120 | ( 121 | p.0, 122 | match p.1 { 123 | QValue::Empty => "".to_string(), 124 | QValue::Value(s) => s, 125 | }, 126 | ) 127 | }) 128 | .collect() 129 | } 130 | 131 | /// Represent the QString as a list of pairs. 132 | /// 133 | /// ``` 134 | /// let qs = qstring::QString::from("?foo=bar&baz=boo"); 135 | /// let ps = qs.to_pairs(); 136 | /// assert_eq!(ps, vec![ 137 | /// ("foo", "bar"), 138 | /// ("baz", "boo"), 139 | /// ]); 140 | /// ``` 141 | pub fn to_pairs(&self) -> Vec<(&str, &str)> { 142 | self.pairs 143 | .iter() 144 | .map(|p| { 145 | ( 146 | p.0.as_str(), 147 | match p.1 { 148 | QValue::Empty => "", 149 | QValue::Value(ref s) => s.as_str(), 150 | }, 151 | ) 152 | }) 153 | .collect() 154 | } 155 | 156 | /// Adds another query parameter pair. 157 | /// 158 | /// ``` 159 | /// let mut qs = qstring::QString::from("?foo=bar&baz=boo"); 160 | /// 161 | /// qs.add_pair(("panda", "bear")); 162 | /// 163 | /// assert_eq!(qs.to_string(), "foo=bar&baz=boo&panda=bear"); 164 | /// ``` 165 | pub fn add_pair(&mut self, pair: (S, T)) 166 | where 167 | S: Into, 168 | T: Into, 169 | { 170 | self.pairs 171 | .push((pair.0.into(), QValue::Value(pair.1.into()))); 172 | } 173 | 174 | /// Parse the string and add all found parameters to this instance. 175 | /// 176 | /// ``` 177 | /// let mut qs = qstring::QString::from("?foo"); 178 | /// 179 | /// qs.add_str("&bar=baz&pooch&panda=bear"); 180 | /// 181 | /// assert_eq!(qs.to_string(), "foo&bar=baz&pooch&panda=bear"); 182 | /// ``` 183 | pub fn add_str(&mut self, origin: &str) { 184 | let mut to_add = str_to_pairs(origin); 185 | self.pairs.append(&mut to_add); 186 | } 187 | 188 | /// The number of query string pairs. 189 | pub fn len(&self) -> usize { 190 | self.pairs.len() 191 | } 192 | 193 | /// if this query string is empty. 194 | pub fn is_empty(&self) -> bool { 195 | self.pairs.is_empty() 196 | } 197 | } 198 | 199 | impl<'a> From<&'a str> for QString { 200 | /// Constructs a new `QString` by parsing a query string part of the URL. 201 | /// Can start with ? or not, either works. 202 | /// 203 | /// Examples 204 | /// 205 | /// ``` 206 | /// let qs = qstring::QString::from("?foo=bar"); 207 | /// let v: Vec<(String, String)> = qs.into_pairs(); 208 | /// assert_eq!(v, vec![("foo".to_string(), "bar".to_string())]); 209 | /// ``` 210 | fn from(origin: &str) -> Self { 211 | QString { 212 | pairs: str_to_pairs(origin), 213 | } 214 | } 215 | } 216 | 217 | fn str_to_pairs(origin: &str) -> Vec<(String, QValue)> { 218 | // current slice left to find params in 219 | let mut cur = origin; 220 | 221 | // move forward if start with ? 222 | if !cur.is_empty() && &cur[0..1] == "?" { 223 | cur = &cur[1..]; 224 | } 225 | 226 | // where we build found parameters into 227 | let mut params = vec![]; 228 | 229 | while !cur.is_empty() { 230 | // if we're positioned on a &, skip it 231 | if &cur[0..1] == "&" { 232 | cur = &cur[1..]; 233 | continue; 234 | } 235 | // find position of next = 236 | let (name, rest) = match cur.find('=') { 237 | // no next =, name will be until next & or until end 238 | None => match cur.find('&') { 239 | // no &, name is until end 240 | None => { 241 | params.push((decode(&cur[..]), QValue::Empty)); 242 | break; 243 | } 244 | // name is until next &, which means no value and shortcut 245 | // to start straight after the &. 246 | Some(pos) => { 247 | params.push((decode(&cur[..pos]), QValue::Empty)); 248 | cur = &cur[(pos + 1)..]; 249 | continue; 250 | } 251 | }, 252 | Some(pos) => { 253 | if let Some(apos) = cur.find('&') { 254 | if apos < pos { 255 | params.push((decode(&cur[..apos]), QValue::Empty)); 256 | cur = &cur[(apos + 1)..]; 257 | continue; 258 | } 259 | } 260 | (&cur[..pos], &cur[(pos + 1)..]) 261 | } 262 | }; 263 | // skip parameters with no name 264 | if name.is_empty() { 265 | cur = rest; 266 | continue; 267 | } 268 | // from rest, find next occurence of & 269 | let (value, newcur) = match rest.find('&') { 270 | // no next &, then value is all up until end 271 | None => (rest, ""), 272 | // found one, value is up until & and next round starts after. 273 | Some(pos) => (&rest[..pos], &rest[(pos + 1)..]), 274 | }; 275 | // found a parameter 276 | params.push((decode(name), QValue::Value(decode(value)))); 277 | cur = newcur; 278 | } 279 | params 280 | } 281 | 282 | impl IntoIterator for QString { 283 | type Item = (String, String); 284 | type IntoIter = ::std::vec::IntoIter<(String, String)>; 285 | fn into_iter(self) -> Self::IntoIter { 286 | self.into_pairs().into_iter() 287 | } 288 | } 289 | 290 | impl Into> for QString { 291 | fn into(self) -> Vec<(String, String)> { 292 | self.into_pairs() 293 | } 294 | } 295 | 296 | impl Into for QString { 297 | fn into(self) -> String { 298 | format!("{}", self) 299 | } 300 | } 301 | 302 | impl ::std::fmt::Display for QString { 303 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 304 | for (idx, p) in self.pairs.iter().enumerate() { 305 | write!( 306 | f, 307 | "{}{}{}", 308 | (if idx == 0 { "" } else { "&" }), 309 | encode(&p.0), 310 | match p.1 { 311 | QValue::Empty => "".to_string(), 312 | QValue::Value(ref s) => format!("={}", encode(s)), 313 | } 314 | )?; 315 | } 316 | Ok(()) 317 | } 318 | } 319 | 320 | fn decode(s: &str) -> String { 321 | percent_decode(s.as_bytes()) 322 | .decode_utf8() 323 | .map(|cow| cow.into_owned()) 324 | .unwrap_or_else(|_| s.to_string()) 325 | } 326 | 327 | const FRAGMENT: &AsciiSet = &CONTROLS 328 | .add(b' ') 329 | .add(b'"') 330 | .add(b'<') 331 | .add(b'>') 332 | .add(b'`') 333 | .add(b'&') 334 | .add(b'?') 335 | .add(b'='); 336 | 337 | fn encode(s: &str) -> String { 338 | utf8_percent_encode(s, FRAGMENT).to_string() 339 | } 340 | 341 | #[cfg(test)] 342 | mod tests { 343 | use super::*; 344 | 345 | macro_rules! test { 346 | ($func_name:ident, $origin:expr, $result:expr) => { 347 | #[test] 348 | fn $func_name() { 349 | let qs = QString::from($origin); 350 | let ps: Vec<(String, String)> = qs.into_pairs(); 351 | let cs: Vec<(String, String)> = ($result as Vec<(&str, &str)>) 352 | .into_iter() 353 | .map(|(k, v)| (k.to_string(), v.to_string())) 354 | .collect(); 355 | assert_eq!(ps, cs); 356 | } 357 | }; 358 | } 359 | 360 | #[test] 361 | fn encode_amp() { 362 | let x = QString::new(vec![("foo", "b&?=ar")]); 363 | assert_eq!("foo=b%26%3F%3Dar", x.to_string()); 364 | } 365 | 366 | #[test] 367 | fn amps_in_a_row() { 368 | assert_eq!( 369 | QString::from("&bar=baz&pooch&panda=bear").to_pairs(), 370 | vec![("bar", "baz"), ("pooch", ""), ("panda", "bear")] 371 | ); 372 | } 373 | 374 | test!(empty_1, "", vec![]); 375 | test!(empty_2, "?", vec![]); 376 | test!(empty_3, "&", vec![]); 377 | test!(empty_4, "=", vec![]); 378 | test!(empty_5, "?=", vec![]); 379 | test!(empty_6, "?&", vec![]); 380 | 381 | test!(a_is_1, "a", vec![("a", "")]); 382 | test!(a_is_2, "a=", vec![("a", "")]); 383 | test!(a_is_3, "a=b", vec![("a", "b")]); 384 | test!(a_is_4, "?a", vec![("a", "")]); 385 | test!(a_is_5, "?a=", vec![("a", "")]); 386 | test!(a_is_6, "?a=b", vec![("a", "b")]); 387 | test!(a_is_7, "?&a", vec![("a", "")]); 388 | test!(a_is_8, "?&a=", vec![("a", "")]); 389 | test!(a_is_9, "?&a=b", vec![("a", "b")]); 390 | test!(a_is_10, "?a=&", vec![("a", "")]); 391 | test!(a_is_11, "?=a", vec![("a", "")]); 392 | 393 | test!(a_is_eq_1, "a==", vec![("a", "=")]); 394 | 395 | test!(is_q_1, "??", vec![("?", "")]); 396 | test!(is_q_2, "&?", vec![("?", "")]); 397 | test!(is_q_3, "??a", vec![("?a", "")]); 398 | test!(is_q_4, "&?a", vec![("?a", "")]); 399 | 400 | test!(ac_is_1, "?a&c", vec![("a", ""), ("c", "")]); 401 | test!(ac_is_2, "?a&c&", vec![("a", ""), ("c", "")]); 402 | test!(ac_is_3, "?a=&c", vec![("a", ""), ("c", "")]); 403 | test!(ac_is_4, "?a=&c=", vec![("a", ""), ("c", "")]); 404 | test!(ac_is_5, "?a=b&c=", vec![("a", "b"), ("c", "")]); 405 | test!(ac_is_6, "?a=&c=d", vec![("a", ""), ("c", "d")]); 406 | test!(ac_is_7, "?a=b&c=d", vec![("a", "b"), ("c", "d")]); 407 | } 408 | --------------------------------------------------------------------------------