Skip to content

Commit

Permalink
feat: Support optional query parameter values (where there is only a …
Browse files Browse the repository at this point in the history
…name)
  • Loading branch information
rholshausen committed Apr 22, 2024
1 parent 758f4c0 commit c3128a6
Show file tree
Hide file tree
Showing 23 changed files with 344 additions and 308 deletions.
168 changes: 71 additions & 97 deletions rust/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion rust/pact_consumer/src/builders/request_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl RequestBuilder {
.get_defaulting()
.entry(key.clone())
.or_insert_with(Default::default)
.push(value.to_example());
.push(Some(value.to_example()));

let mut path = DocPath::root();
path.push_field(key);
Expand Down
1 change: 1 addition & 0 deletions rust/pact_ffi/src/log/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::log::inmem_buffer::InMemBuffer;

/// A sink for logs to be written to, based on a provider specifier.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum Sink {
/// Write logs to stdout.
Stdout(Stdout),
Expand Down
105 changes: 60 additions & 45 deletions rust/pact_ffi/src/mock_server/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,11 +771,11 @@ pub extern fn pactffi_with_query_parameter(
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value;
values[index] = Some(value);
} else {
let mut values: Vec<String> = Vec::new();
let mut values: Vec<Option<String>> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value;
values[index] = Some(value);
q.insert(name.to_string(), values);
};
q
Expand All @@ -784,9 +784,9 @@ pub extern fn pactffi_with_query_parameter(
path.push_field(name).push_index(index);
#[allow(deprecated)]
let value = from_integration_json(&mut reqres.request.matching_rules, &mut reqres.request.generators, &value.to_string(), path, "query");
let mut values: Vec<String> = Vec::new();
let mut values: Vec<Option<String>> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value;
values[index] = Some(value);
Some(hashmap! { name.to_string() => values })
});
!mock_server_started
Expand Down Expand Up @@ -839,8 +839,12 @@ pub extern fn pactffi_with_query_parameter(
/// const char* value = "{\"value\":[\"2\"], \"pact:matcher:type\":\"regex\", \"regex\":\"\\\\d+\"}";
/// pactffi_with_query_parameter_v2(handle, "id", 0, value);
/// ```
///
/// For a query parameter with no value, the value parameter can be set to a NULL pointer.
///
/// # Safety
/// The name and value parameters must be valid pointers to NULL terminated strings.
/// The name parameter must be a valid pointer to a NULL terminated string. If the value
/// parameter is not NULL, it must point to a valid NULL terminated string.
/// ```
#[no_mangle]
pub extern fn pactffi_with_query_parameter_v2(
Expand All @@ -850,7 +854,7 @@ pub extern fn pactffi_with_query_parameter_v2(
value: *const c_char
) -> bool {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
let value = convert_cstr("value", value);
trace!(?interaction, name, index, value, "pactffi_with_query_parameter_v2 called");
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
Expand All @@ -860,31 +864,38 @@ pub extern fn pactffi_with_query_parameter_v2(
path.push_index(index);
}

let value = from_integration_json_v2(
&mut reqres.request.matching_rules,
&mut reqres.request.generators,
value,
path,
"query",
index
);
match value {
Either::Left(value) => {
reqres.request.query = update_query_map(index, name, reqres, &value);
}
Either::Right(values) => if index == 0 {
reqres.request.query = reqres.request.query.clone().map(|mut q| {
if q.contains_key(name) {
let vec = q.get_mut(name).unwrap();
vec.extend_from_slice(&values);
} else {
q.insert(name.to_string(), values.clone());
};
q
}).or_else(|| Some(hashmap! { name.to_string() => values }))
} else {
reqres.request.query = update_query_map(index, name, reqres, &values.first().cloned().unwrap_or_default());
if let Some(value) = value {
let value = from_integration_json_v2(
&mut reqres.request.matching_rules,
&mut reqres.request.generators,
value,
path,
"query",
index
);
match value {
Either::Left(value) => {
reqres.request.query = update_query_map(index, name, reqres, Some(value));
}
Either::Right(values) => if index == 0 {
reqres.request.query = reqres.request.query.clone().map(|mut q| {
let values = values.iter().map(|v| Some(v.clone())).collect_vec();
if q.contains_key(name) {
let vec = q.get_mut(name).unwrap();
vec.extend_from_slice(&values);
} else {
q.insert(name.to_string(), values);
};
q
}).or_else(|| Some(hashmap! {
name.to_string() => values.iter().map(|v| Some(v.clone())).collect_vec()
}))
} else {
reqres.request.query = update_query_map(index, name, reqres, values.first().cloned());
}
}
} else {
reqres.request.query = update_query_map(index, name, reqres, None);
}
!mock_server_started
} else {
Expand All @@ -898,25 +909,29 @@ pub extern fn pactffi_with_query_parameter_v2(
}
}

fn update_query_map(index: size_t, name: &str, reqres: &mut SynchronousHttp, value: &String) -> Option<HashMap<String, Vec<String>>> {
fn update_query_map(
index: size_t,
name: &str,
reqres: &mut SynchronousHttp,
value: Option<String>
) -> Option<HashMap<String, Vec<Option<String>>>> {
reqres.request.query.clone().map(|mut q| {
if q.contains_key(name) {
let values = q.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
if let Some(entry) = q.get_mut(name) {
if index >= entry.len() {
entry.resize_with(index + 1, Default::default);
}
values[index] = value.clone();
entry[index] = value.clone();
} else {
let mut values: Vec<String> = Vec::new();
let mut values: Vec<Option<String>> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.clone();
q.insert(name.to_string(), values);
};
q
}).or_else(|| {
let mut values: Vec<String> = Vec::new();
let mut values: Vec<Option<String>> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.clone();
values[index] = value;
Some(hashmap! { name.to_string() => values })
})
}
Expand Down Expand Up @@ -2866,7 +2881,7 @@ mod tests {
pactffi_free_pact_handle(pact_handle);

expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string()]
"id".to_string() => vec![Some("100".to_string())]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::QUERY).cloned().unwrap_or_default().is_empty()).to(be_true());
}
Expand All @@ -2888,7 +2903,7 @@ mod tests {
pactffi_free_pact_handle(pact_handle);

expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string()]
"id".to_string() => vec![Some("100".to_string())]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"query" => { "$.id" => [ MatchingRule::Regex("\\d+".to_string()) ] }
Expand All @@ -2912,7 +2927,7 @@ mod tests {
pactffi_free_pact_handle(pact_handle);

expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["1".to_string(), "2".to_string()]
"id".to_string() => vec![Some("1".to_string()), Some("2".to_string())]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::QUERY).cloned().unwrap_or_default().is_empty()).to(be_true());
}
Expand All @@ -2936,7 +2951,7 @@ mod tests {
pactffi_free_pact_handle(pact_handle);

expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string(), "abc".to_string()]
"id".to_string() => vec![Some("100".to_string()), Some("abc".to_string())]
}));
assert_eq!(&interaction.request.matching_rules, &matchingrules! {
"query" => {
Expand Down Expand Up @@ -2964,7 +2979,7 @@ mod tests {
pactffi_free_pact_handle(pact_handle);

expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"catId[]".to_string() => vec!["1".to_string()]
"catId[]".to_string() => vec![Some("1".to_string())]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"query" => { "$['catId[]']" => [ MatchingRule::MinType(1) ] }
Expand Down
2 changes: 1 addition & 1 deletion rust/pact_ffi/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn create_query_parameter_with_multiple_values() {
interaction.with_interaction(&|_, _, i| {
let interaction = i.as_v4_http().unwrap();
expect!(interaction.request.query.as_ref()).to(be_some().value(&hashmap!{
"q".to_string() => vec!["1".to_string(), "2".to_string(), "3".to_string()]
"q".to_string() => vec![Some("1".to_string()), Some("2".to_string()), Some("3".to_string())]
}));
});
}
Expand Down
6 changes: 3 additions & 3 deletions rust/pact_matching/src/form_urlencoded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ pub(crate) fn match_form_urlencoded(
(Ok(e), Ok(a)) => {
let expected_params = super::group_by(e, |(k, _)| k.clone())
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|(_, v)| v.clone()).collect_vec()))
.map(|(k, v)| (k.clone(), v.iter().map(|(_, v)| Some(v.clone())).collect_vec()))
.collect();
let actual_params = super::group_by(a, |(k, _)| k.clone())
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|(_, v)| v.clone()).collect_vec()))
.map(|(k, v)| (k.clone(), v.iter().map(|(_, v)| Some(v.clone())).collect_vec()))
.collect();
let result: Vec<_> = match_query_maps(expected_params, actual_params, context)
.values().flat_map(|m| m.iter().map(|mismatch| {
Expand Down Expand Up @@ -220,7 +220,7 @@ mod tests {
path: "$.a".to_string(),
expected: Some("[\"b\"]".into()),
actual: Some("".into()),
mismatch: "".to_string(),
mismatch: "Expected form post parameter 'a' but was missing".to_string()
});
assert_eq!(mismatches[0].description(), "$.a -> Expected form post parameter 'a' but was missing");
}
Expand Down
16 changes: 8 additions & 8 deletions rust/pact_matching/src/generator_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,25 @@ async fn applies_header_generator_for_headers_to_the_copy_of_the_request() {
#[tokio::test]
async fn applies_query_generator_for_query_parameters_to_the_copy_of_the_request() {
let request = HttpRequest { query: Some(hashmap!{
"A".to_string() => vec![ "a".to_string() ],
"B".to_string() => vec![ "b".to_string() ]
"A".to_string() => vec![ Some("a".to_string()) ],
"B".to_string() => vec![ Some("b".to_string()) ]
}), generators: generators! {
"QUERY" => {
"A" => Generator::Uuid(None)
}
}, .. HttpRequest::default()
};
let query = generate_request(&request, &GeneratorTestMode::Provider, &hashmap!{}).await.query.unwrap().clone();
let query_val = &query.get("A").unwrap()[0];
expect!(query_val).to_not(be_equal_to("a"));
let query_val = query.get("A").unwrap()[0].as_ref();
expect!(query_val.unwrap()).to_not(be_equal_to("a"));
}

#[test_log::test(tokio::test)]
async fn applies_provider_state_generator_for_query_parameters_with_square_brackets() {
let request = HttpRequest {
query: Some(hashmap!{
"A".to_string() => vec![ "a".to_string() ],
"q[]".to_string() => vec![ "q1".to_string(), "q2".to_string() ]
"A".to_string() => vec![ Some("a".to_string()) ],
"q[]".to_string() => vec![ Some("q1".to_string()), Some("q2".to_string()) ]
}),
generators: generators! {
"QUERY" => {
Expand All @@ -109,9 +109,9 @@ async fn applies_provider_state_generator_for_query_parameters_with_square_brack
let result = generate_request(&request, &GeneratorTestMode::Provider, &context).await;
let query = result.query.unwrap();
let a_val = query.get("A").unwrap();
expect!(a_val).to(be_equal_to(&vec!["1234".to_string()]));
expect!(a_val).to(be_equal_to(&vec![Some("1234".to_string())]));
let q_val = query.get("q[]").unwrap();
expect!(q_val).to(be_equal_to(&vec!["5678".to_string(), "5678".to_string()]));
expect!(q_val).to(be_equal_to(&vec![Some("5678".to_string()), Some("5678".to_string())]));
}

#[tokio::test]
Expand Down
35 changes: 17 additions & 18 deletions rust/pact_matching/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,8 +789,8 @@ impl CommonMismatch {
pub fn to_query_mismatch(&self) -> Mismatch {
Mismatch::QueryMismatch {
parameter: self.path.clone(),
expected: self.expected.clone().into(),
actual: self.actual.clone().into(),
expected: self.expected.clone(),
actual: self.actual.clone(),
mismatch: self.description.clone()
}
}
Expand Down Expand Up @@ -1426,24 +1426,26 @@ pub fn match_path(expected: &str, actual: &str, context: &(dyn MatchingContext +

/// Matches the actual query parameters to the expected ones.
pub fn match_query(
expected: Option<HashMap<String, Vec<String>>>,
actual: Option<HashMap<String, Vec<String>>>,
expected: Option<HashMap<String, Vec<Option<String>>>>,
actual: Option<HashMap<String, Vec<Option<String>>>>,
context: &(dyn MatchingContext + Send + Sync)
) -> HashMap<String, Vec<Mismatch>> {
match (actual, expected) {
(Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
(Some(aqm), None) => aqm.iter().map(|(key, value)| {
let actual_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
(key.clone(), vec![Mismatch::QueryMismatch {
parameter: key.clone(),
expected: "".to_string(),
actual: format!("{:?}", value),
actual: format!("{:?}", actual_value),
mismatch: format!("Unexpected query parameter '{}' received", key)
}])
}).collect(),
(None, Some(eqm)) => eqm.iter().map(|(key, value)| {
let expected_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
(key.clone(), vec![Mismatch::QueryMismatch {
parameter: key.clone(),
expected: format!("{:?}", value),
expected: format!("{:?}", expected_value),
actual: "".to_string(),
mismatch: format!("Expected query parameter '{}' but was missing", key)
}])
Expand Down Expand Up @@ -2103,22 +2105,19 @@ pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, c
if let Some(parameter) = parameters.get_mut(param) {
let mut generated = parameter.clone();
for (index, val) in parameter.iter().enumerate() {
if let Ok(v) = generator.generate_value(val, context, &DefaultVariantMatcher.boxed()) {
generated[index] = v;
let value = val.clone().unwrap_or_default();
if let Ok(v) = generator.generate_value(&value, context, &DefaultVariantMatcher.boxed()) {
generated[index] = Some(v);
}
}
*parameter = generated;
} else {
if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
parameters.insert(param.to_string(), vec![ v.to_string() ]);
}
}
} else {
if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
request.query = Some(hashmap!{
param.to_string() => vec![ v.to_string() ]
})
} else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
parameters.insert(param.to_string(), vec![ Some(v.to_string()) ]);
}
} else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
request.query = Some(hashmap!{
param.to_string() => vec![ Some(v.to_string()) ]
})
}
}
});
Expand Down
Loading

0 comments on commit c3128a6

Please sign in to comment.