use std::collections::HashMap;
use std::str::from_utf8;
use hex::FromHex;
use itertools::Itertools;
use serde_json::Value;
use tracing::{error, trace, warn};
use crate::PactSpecification;
pub fn decode_query(query: &str) -> Result<String, String> {
let mut chars = query.chars();
let mut ch = chars.next();
let mut buffer = vec![];
while ch.is_some() {
let c = ch.unwrap();
trace!("ch = '{:?}'", ch);
if c == '%' {
let c1 = chars.next();
let c2 = chars.next();
match (c1, c2) {
(Some(v1), Some(v2)) => {
let mut s = String::new();
s.push(v1);
s.push(v2);
let decoded: Result<Vec<u8>, _> = FromHex::from_hex(s.into_bytes());
match decoded {
Ok(n) => {
trace!("decoded = '{:?}'", n);
buffer.extend_from_slice(&n);
},
Err(err) => {
error!("Failed to decode '%{}{}' to as HEX - {}", v1, v2, err);
buffer.push('%' as u8);
buffer.push(v1 as u8);
buffer.push(v2 as u8);
}
}
},
(Some(v1), None) => {
buffer.push('%' as u8);
buffer.push(v1 as u8);
},
_ => buffer.push('%' as u8)
}
} else if c == '+' {
buffer.push(' ' as u8);
} else {
buffer.push(c as u8);
}
ch = chars.next();
}
match from_utf8(&buffer) {
Ok(s) => Ok(s.to_owned()),
Err(err) => {
error!("Failed to decode '{}' to UTF-8 - {}", query, err);
Err(format!("Failed to decode '{}' to UTF-8 - {}", query, err))
}
}
}
pub fn encode_query(query: &str) -> String {
query.chars().map(|ch| {
match ch {
' ' => "+".to_string(),
'-' => ch.to_string(),
'a'..='z' => ch.to_string(),
'A'..='Z' => ch.to_string(),
'0'..='9' => ch.to_string(),
_ => ch.escape_unicode()
.filter(|u| u.is_digit(16))
.batching(|it| {
match it.next() {
None => None,
Some(x) => Some((x, it.next().unwrap()))
}
})
.map(|u| format!("%{}{}", u.0, u.1))
.collect()
}
}).collect()
}
pub fn parse_query_string(query: &str) -> Option<HashMap<String, Vec<Option<String>>>> {
if !query.is_empty() {
Some(query.split('&').map(|kv| {
trace!("kv = '{}'", kv);
if kv.is_empty() {
vec![]
} else {
kv.splitn(2, '=').collect::<Vec<&str>>()
}
}).fold(HashMap::new(), |mut map, name_value| {
trace!("name_value = '{:?}'", name_value);
if !name_value.is_empty() {
let name = decode_query(name_value[0])
.unwrap_or_else(|_| name_value[0].to_owned());
let value = if name_value.len() > 1 {
Some(decode_query(name_value[1]).unwrap_or_else(|_| name_value[1].to_owned()))
} else {
None
};
trace!("decoded: '{}' => {:?}", name, value);
map.entry(name).or_insert_with(|| vec![]).push(value);
}
map
}))
} else {
None
}
}
pub fn build_query_string(query: HashMap<String, Vec<Option<String>>>) -> String {
query.into_iter()
.filter(|(k, _)| !k.is_empty())
.sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
.flat_map(|kv| {
kv.1.iter()
.map(|v| match v {
None => kv.0.clone(),
Some(s) => format!("{}={}", kv.0, encode_query(s))
})
.collect_vec()
})
.join("&")
}
pub fn query_from_json(query_json: &Value, spec_version: &PactSpecification) -> Option<HashMap<String, Vec<Option<String>>>> {
match query_json {
Value::String(s) => parse_query_string(s),
_ => {
warn!("Only string versions of request query strings are supported with specification version {}, ignoring.",
spec_version.to_string());
None
}
}
}
pub fn v3_query_from_json(
query_json: &Value,
spec_version: &PactSpecification
) -> Option<HashMap<String, Vec<Option<String>>>> {
match query_json {
Value::String(s) => parse_query_string(s),
Value::Object(map) => Some(map.iter().map(|(k, v)| {
(k.clone(), match v {
Value::String(s) => vec![Some(s.clone())],
Value::Array(array) => array.iter().map(|item| match item {
Value::String(s) => Some(s.clone()),
Value::Null => None,
_ => Some(v.to_string())
}).collect(),
_ => {
warn!("Query parameter value '{}' is not valid, ignoring", v);
vec![]
}
})
}).collect()),
_ => {
warn!("Only string or map versions of request query strings are supported with specification version {}, ignoring.",
spec_version.to_string());
None
}
}
}
pub fn query_to_json(query: HashMap<String, Vec<Option<String>>>, spec_version: &PactSpecification) -> Value {
match spec_version {
PactSpecification::V3 | PactSpecification::V4 => Value::Object(query
.iter()
.sorted_by(|(a, _), (b, _)| Ord::cmp(a, b))
.map(|(k, v)| {
(k.clone(), Value::Array(v.iter().map(|q| match q {
None => Value::Null,
Some(s) => Value::String(s.clone())
}).collect()))
})
.collect()),
_ => Value::String(build_query_string(query))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use expectest::prelude::*;
use maplit::hashmap;
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::query_strings::parse_query_string;
#[test]
fn parse_query_string_test() {
let query = "a=b&c=d".to_string();
let expected = hashmap!{
"a".to_string() => vec![Some("b".to_string())],
"c".to_string() => vec![Some("d".to_string())]
};
let result = parse_query_string(&query);
expect!(result).to(be_some().value(expected));
}
#[test]
fn parse_query_string_handles_empty_string() {
let query = "".to_string();
let expected = None;
let result = parse_query_string(&query);
assert_eq!(result, expected);
}
#[test]
fn parse_query_string_handles_missing_values() {
let query = "a=&c=d".to_string();
let mut expected = HashMap::new();
expected.insert("a".to_string(), vec![Some("".to_string())]);
expected.insert("c".to_string(), vec![Some("d".to_string())]);
let result = parse_query_string(&query);
assert_eq!(result, Some(expected));
}
#[test]
fn parse_query_string_handles_equals_in_values() {
let query = "a=b&c=d=e=f".to_string();
let mut expected = HashMap::new();
expected.insert("a".to_string(), vec![Some("b".to_string())]);
expected.insert("c".to_string(), vec![Some("d=e=f".to_string())]);
let result = parse_query_string(&query);
assert_eq!(result, Some(expected));
}
#[test]
fn parse_query_string_decodes_values() {
let query = "a=a%20b%20c".to_string();
let expected = hashmap! {
"a".to_string() => vec![Some("a b c".to_string())]
};
let result = parse_query_string(&query);
expect!(result).to(be_some().value(expected));
}
#[test]
fn parse_query_string_decodes_non_ascii_values() {
let query = "accountNumber=100&anotherValue=%E6%96%87%E4%BB%B6.txt".to_string();
let expected = hashmap! {
"accountNumber".to_string() => vec![Some("100".to_string())],
"anotherValue".to_string() => vec![Some("文件.txt".to_string())]
};
let result = parse_query_string(&query);
expect!(result).to(be_some().value(expected));
}
#[test]
fn parse_query_string_handles_no_values() {
let query = "a&c=&c&a".to_string();
let mut expected = HashMap::new();
expected.insert("a".to_string(), vec![None, None]);
expected.insert("c".to_string(), vec![Some("".to_string()), None]);
let result = parse_query_string(&query);
assert_eq!(result, Some(expected));
}
#[rstest]
#[case(hashmap!{}, "")]
#[case(hashmap!{ "A".to_string() => vec![] }, "")]
#[case(hashmap!{ "A".to_string() => vec![ Some("B".to_string()) ] }, "A=B")]
#[case(hashmap!{ "".to_string() => vec![ Some("B".to_string()) ] }, "")]
#[case(hashmap!{ "A".to_string() => vec![ Some("B".to_string()), Some("c".to_string()) ] }, "A=B&A=c")]
#[case(hashmap!{ "A".to_string() => vec![ Some("B".to_string()) ], "b".to_string() => vec![ Some("c".to_string()) ] }, "A=B&b=c")]
#[case(hashmap!{ "A".to_string() => vec![ Some("".to_string()) ] }, "A=")]
#[case(hashmap!{ "A".to_string() => vec![ Some("".to_string()), Some("".to_string()) ] }, "A=&A=")]
#[case(hashmap!{ "A".to_string() => vec![ None ] }, "A")]
#[case(hashmap!{ "A".to_string() => vec![ None, None ] }, "A&A")]
fn build_query_string_test(#[case] map: HashMap<String, Vec<Option<String>>>, #[case] expected: &str) {
let result = super::build_query_string(map);
assert_eq!(result, expected)
}
}