diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 364439bf8..54697960b 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -2221,6 +2221,7 @@ dependencies = [
"rstest 0.22.0",
"serde",
"serde_json",
+ "serde_urlencoded",
"sxd-document",
"tempfile",
"test-log",
diff --git a/rust/pact_ffi/Cargo.toml b/rust/pact_ffi/Cargo.toml
index 749583544..c7b837671 100644
--- a/rust/pact_ffi/Cargo.toml
+++ b/rust/pact_ffi/Cargo.toml
@@ -52,6 +52,7 @@ tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "tracing-log"] }
uuid = { version = "1.10.0", features = ["v4"] }
zeroize = "1.8.1"
+serde_urlencoded = "0.7.1"
[dev-dependencies]
expectest = "0.12.0"
diff --git a/rust/pact_ffi/src/mock_server/form_urlencoded.rs b/rust/pact_ffi/src/mock_server/form_urlencoded.rs
new file mode 100644
index 000000000..4b1fdb286
--- /dev/null
+++ b/rust/pact_ffi/src/mock_server/form_urlencoded.rs
@@ -0,0 +1,235 @@
+//! Form UrlEncoded matching support
+
+use std::collections::HashMap;
+use serde_json::Value;
+use tracing::{debug, error, trace};
+
+use pact_models::generators::{GeneratorCategory, Generators};
+use pact_models::generators::form_urlencoded::QueryParams;
+use pact_models::matchingrules::MatchingRuleCategory;
+use pact_models::path_exp::DocPath;
+
+use crate::mock_server::bodies::process_json;
+
+/// Process a JSON body with embedded matching rules and generators
+pub fn process_form_urlencoded_json(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> String {
+ trace!("process_form_urlencoded_json");
+ let json = process_json(body, matching_rules, generators);
+ debug!("form_urlencoded json: {json}");
+ let values: Value = serde_json::from_str(json.as_str()).unwrap();
+ debug!("form_urlencoded values: {values}");
+ let params = convert_json_value_to_query_params(values, matching_rules, generators);
+ debug!("form_urlencoded params: {:?}", params);
+ serde_urlencoded::to_string(params).expect("could not serialize body to form urlencoded string")
+}
+
+fn convert_json_value_to_query_params(value: Value, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> QueryParams {
+ let mut params: QueryParams = vec![];
+ match value {
+ Value::Object(map) => {
+ for (key, value) in map.iter() {
+ let path = DocPath::root().join(key);
+ match value {
+ Value::Number(value) => params.push((key.clone(), value.to_string())),
+ Value::String(value) => params.push((key.clone(), value.to_string())),
+ Value::Array(vec) => {
+ for (index, value) in vec.iter().enumerate() {
+ let path = DocPath::root().join(key).join_index(index);
+ match value {
+ Value::Number(value) => params.push((key.clone(), value.to_string())),
+ Value::String(value) => params.push((key.clone(), value.to_string())),
+ _ => handle_form_urlencoded_invalid_value(value, &path, matching_rules, generators),
+ }
+ }
+ },
+ _ => handle_form_urlencoded_invalid_value(value, &path, matching_rules, generators),
+ }
+ }
+ },
+ _ => ()
+ }
+ params
+}
+
+fn handle_form_urlencoded_invalid_value(value: &Value, path: &DocPath, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) {
+ for key in matching_rules.clone().rules.keys() {
+ if String::from(key).contains(&String::from(path)) {
+ matching_rules.rules.remove(&key);
+ generators.categories.entry(GeneratorCategory::BODY).or_insert(HashMap::new()).remove(&key);
+ }
+ }
+ error!("Value '{:?}' is not supported in form urlencoded. Matchers and generators (if defined) are removed", value);
+}
+
+#[cfg(test)]
+mod test {
+ use expectest::prelude::*;
+ use rstest::rstest;
+ use serde_json::json;
+
+ use pact_models::generators;
+ use pact_models::generators::Generator;
+ use pact_models::matchingrules_list;
+ use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory};
+ use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType};
+
+ use super::*;
+
+ #[rstest]
+ #[case(
+ json!({ "": "empty key" }),
+ "=empty+key",
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "": ["first", "second", "third"] }),
+ "=first&=second&=third",
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "": { "pact:matcher:type": "includes", "value": "empty" } }),
+ "",
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "number_value": -123.45 }),
+ "number_value=-123.45".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "string_value": "hello world" }),
+ "string_value=hello+world".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_values": [null, 234, "example text", {"key": "value"}, ["value 1", "value 2"]] }),
+ "array_values=234&array_values=example+text".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "null_value": null }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "null_value_with_matcher": { "pact:matcher:type": "null" } }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "number_value_with_matcher": { "pact:matcher:type": "number", "min": 0, "max": 10, "value": 123 } }),
+ "number_value_with_matcher=123".to_string(),
+ matchingrules_list!{"body"; "$.number_value_with_matcher" => [MatchingRule::Number]},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "number_value_with_matcher_and_generator": { "pact:matcher:type": "number", "pact:generator:type": "RandomInt", "min": 0, "max": 10, "value": 123 } }),
+ "number_value_with_matcher_and_generator=123".to_string(),
+ matchingrules_list!{"body"; "$.number_value_with_matcher_and_generator" => [MatchingRule::Number]},
+ generators! {"BODY" => {"$.number_value_with_matcher_and_generator" => Generator::RandomInt(0, 10)}}
+ )]
+ // Missing value => null will be used => but it is not supported, so matcher is removed.
+ #[case(
+ json!({ "number_matcher_only": { "pact:matcher:type": "number", "min": 0, "max": 10 } }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "string_value_with_matcher_and_generator": { "pact:matcher:type": "type", "value": "some string", "pact:generator:type": "RandomString", "size": 15 } }),
+ "string_value_with_matcher_and_generator=some+string".to_string(),
+ matchingrules_list!{"body"; "$.string_value_with_matcher_and_generator" => [MatchingRule::Type]},
+ generators! {"BODY" => {"$.string_value_with_matcher_and_generator" => Generator::RandomString(15)}}
+ )]
+ #[case(
+ json!({ "string_value_with_matcher": { "pact:matcher:type": "type", "value": "some string", "size": 15 } }),
+ "string_value_with_matcher=some+string".to_string(),
+ matchingrules_list!{"body"; "$.string_value_with_matcher" => [MatchingRule::Type]},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_values_with_matcher": { "pact:matcher:type": "eachValue", "value": ["string value"], "rules": [{ "pact:matcher:type": "type", "value": "string" }] } }),
+ "array_values_with_matcher=string+value".to_string(),
+ matchingrules_list!{"body"; "$.array_values_with_matcher" => [MatchingRule::EachValue(MatchingRuleDefinition::new("[\"string value\"]".to_string(), ValueType::Unknown, MatchingRule::Type, None))]},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_values_with_matcher_and_generator": [
+ { "pact:matcher:type": "regex", "value": "a1", "pact:generator:type": "Regex", "regex": "\\w\\d" },
+ { "pact:matcher:type": "decimal", "pact:generator:type": "RandomDecimal", "digits": 3, "value": 12.3 }
+ ] }),
+ "array_values_with_matcher_and_generator=a1&array_values_with_matcher_and_generator=12.3".to_string(),
+ matchingrules_list!{
+ "body";
+ "$.array_values_with_matcher_and_generator[0]" => [MatchingRule::Regex("\\w\\d".to_string())],
+ "$.array_values_with_matcher_and_generator[1]" => [MatchingRule::Decimal]
+ },
+ generators! {"BODY" => {
+ "$.array_values_with_matcher_and_generator[0]" => Generator::Regex("\\w\\d".to_string()),
+ "$.array_values_with_matcher_and_generator[1]" => Generator::RandomDecimal(3)
+ }}
+ )]
+ #[case(
+ json!({ "false": false }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "true": true }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_of_false": [false] }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_of_true": [true] }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_of_objects": [{ "key": "value" }] }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "array_of_arrays": [["value 1", "value 2"]] }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(
+ json!({ "object_value": { "key": "value" } }),
+ "".to_string(), matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(json!(
+ { "boolean_with_matcher_and_generator": { "pact:matcher:type": "boolean", "value": true, "pact:generator:type": "RandomBoolean" } }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ #[case(json!(
+ { "object_with_matcher_and_generator": { "pact:matcher:type": "type", "value": {"key": { "pact:matcher:type": "type", "value": "value", "pact:generator:type": "RandomString" }} } }),
+ "".to_string(),
+ matchingrules_list!{"body"; "$" => []},
+ generators! {"BODY" => {}}
+ )]
+ fn process_form_urlencoded_json_test(#[case] json: Value, #[case] result: String, #[case] expected_matching_rules: MatchingRuleCategory, #[case] expected_generators: Generators) {
+ let mut matching_rules = MatchingRuleCategory::empty("body");
+ let mut generators = Generators::default();
+ expect!(process_form_urlencoded_json(json.to_string(), &mut matching_rules, &mut generators)).to(be_equal_to(result));
+ expect!(matching_rules).to(be_equal_to(expected_matching_rules));
+ expect!(generators).to(be_equal_to(expected_generators));
+ }
+}
diff --git a/rust/pact_ffi/src/mock_server/handles.rs b/rust/pact_ffi/src/mock_server/handles.rs
index be8614921..393fda369 100644
--- a/rust/pact_ffi/src/mock_server/handles.rs
+++ b/rust/pact_ffi/src/mock_server/handles.rs
@@ -160,6 +160,7 @@ use crate::mock_server::bodies::{
get_content_type_hint,
part_body_replace_marker
};
+use crate::mock_server::form_urlencoded::process_form_urlencoded_json;
use crate::models::iterators::{PactAsyncMessageIterator, PactMessageIterator, PactSyncHttpIterator, PactSyncMessageIterator};
use crate::ptr;
@@ -1700,6 +1701,11 @@ fn process_body(
matching_rules,
generators
);
+
+ if body.is_empty() {
+ return OptionalBody::Empty;
+ }
+
let detected_type = detect_content_type_from_string(body);
let content_type = content_type
.clone()
@@ -1744,18 +1750,35 @@ fn process_body(
}
_ => {
trace!("Raw XML body left as is");
- OptionalBody::from(body)
+ OptionalBody::Present(Bytes::from(body.to_owned()), Some(ct), None)
+ }
+ }
+ }
+ Some(ct) if ct.is_form_urlencoded() => {
+ // The Form UrlEncoded payload may contain one of two cases:
+ // 1. A raw Form UrlEncoded payload
+ // 2. A JSON payload describing the Form UrlEncoded payload, including any
+ // embedded generators and matching rules.
+ match detected_type {
+ Some(detected_ct) if detected_ct.is_json() => {
+ trace!("Processing JSON description for Form UrlEncoded body");
+ let category = matching_rules.add_category("body");
+ OptionalBody::Present(
+ Bytes::from(process_form_urlencoded_json(body.to_string(), category, generators)),
+ Some(ct), // Note to use the provided content type, not the detected one
+ None,
+ )
+ }
+ _ => {
+ trace!("Raw Form UrlEncoded body left as is");
+ OptionalBody::Present(Bytes::from(body.to_owned()), Some(ct), None)
}
}
}
_ => {
// We either have no content type, or an unsupported content type.
trace!("Raw body");
- if body.is_empty() {
- OptionalBody::Empty
- } else {
- OptionalBody::Present(Bytes::from(body.to_owned()), content_type, None)
- }
+ OptionalBody::Present(Bytes::from(body.to_owned()), content_type, None)
}
}
}
@@ -3203,6 +3226,7 @@ mod tests {
use pact_models::path_exp::DocPath;
use pact_models::prelude::{Generators, MatchingRules};
use pretty_assertions::assert_eq;
+ use rstest::rstest;
use crate::mock_server::handles::*;
@@ -4337,14 +4361,16 @@ mod tests {
// See https://github.com/pact-foundation/pact-php/pull/626
// and https://github.com/pact-foundation/pact-reference/pull/461
- #[test]
- fn annotate_raw_body_branch() {
+ #[rstest]
+ #[case("a=1&b=2&c=3", "application/x-www-form-urlencoded")]
+ #[case(r#"- text
"#, "application/xml")]
+ fn pactffi_with_raw_body_test(#[case] raw: String, #[case] ct: String) {
let pact_handle = PactHandle::new("Consumer", "Provider");
let description = CString::new("Generator Test").unwrap();
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
- let body = CString::new("a=1&b=2&c=3").unwrap();
- let content_type = CString::new("application/x-www-form-urlencoded").unwrap();
+ let body = CString::new(raw.clone()).unwrap();
+ let content_type = CString::new(ct.clone()).unwrap();
let result = pactffi_with_body(
i_handle,
InteractionPart::Request,
@@ -4363,11 +4389,11 @@ mod tests {
.headers
.expect("no headers found")
.get("Content-Type"),
- Some(&vec!["application/x-www-form-urlencoded".to_string()])
+ Some(&vec![ct])
);
assert_eq!(
interaction.request.body.value(),
- Some(Bytes::from("a=1&b=2&c=3"))
+ Some(Bytes::from(raw))
)
}
@@ -4423,4 +4449,35 @@ mod tests {
expect!(result_1).to(be_false());
expect!(result_2).to(be_false());
}
+
+ #[test]
+ fn pactffi_with_empty_body_test() {
+ let pact_handle = PactHandle::new("Consumer", "Provider");
+ let description = CString::new("Generator Test").unwrap();
+ let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
+
+ let body = CString::new("").unwrap();
+ let content_type = CString::new("text/plain").unwrap();
+ let result = pactffi_with_body(
+ i_handle,
+ InteractionPart::Request,
+ content_type.as_ptr(),
+ body.as_ptr(),
+ );
+ assert!(result);
+
+ let interaction = i_handle
+ .with_interaction(&|_, _, inner| inner.as_v4_http().unwrap())
+ .unwrap();
+
+ expect!(
+ interaction
+ .request
+ .headers
+ ).to(be_none());
+ assert_eq!(
+ interaction.request.body.value(),
+ None
+ )
+ }
}
diff --git a/rust/pact_ffi/src/mock_server/mod.rs b/rust/pact_ffi/src/mock_server/mod.rs
index 2fd59b57f..c8bc41e5c 100644
--- a/rust/pact_ffi/src/mock_server/mod.rs
+++ b/rust/pact_ffi/src/mock_server/mod.rs
@@ -78,6 +78,7 @@ use crate::string::optional_str;
pub mod handles;
pub mod bodies;
mod xml;
+mod form_urlencoded;
/// [DEPRECATED] External interface to create a HTTP mock server. A pointer to the pact JSON as a NULL-terminated C
/// string is passed in, as well as the port for the mock server to run on. A value of 0 for the
diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs
index 517f82c7a..3f4a37229 100644
--- a/rust/pact_ffi/tests/tests.rs
+++ b/rust/pact_ffi/tests/tests.rs
@@ -1841,3 +1841,107 @@ fn returns_mock_server_logs() {
assert_ne!(logs,"", "logs are empty");
}
+
+#[test]
+#[allow(deprecated)]
+fn http_form_urlencoded_consumer_feature_test() {
+ let consumer_name = CString::new("http-consumer").unwrap();
+ let provider_name = CString::new("http-provider").unwrap();
+ let pact_handle = pactffi_new_pact(consumer_name.as_ptr(), provider_name.as_ptr());
+ let description = CString::new("form_urlencoded_request_with_matchers").unwrap();
+ let interaction = pactffi_new_interaction(pact_handle.clone(), description.as_ptr());
+ let accept_header = CString::new("Accept").unwrap();
+ let content_type_header = CString::new("Content-Type").unwrap();
+ let json = json!({
+ "number": {
+ "pact:matcher:type": "number",
+ "value": 23.45
+ },
+ "string": {
+ "pact:matcher:type": "type",
+ "value": "example text"
+ },
+ "array": {
+ "pact:matcher:type": "eachValue(matching(regex, 'value1|value2|value3|value4', 'value2'))",
+ "value": ["value1", "value4"]
+ }
+ });
+ let body = CString::new(json.to_string()).unwrap();
+ let response_json = json!({
+ "number": {
+ "pact:matcher:type": "number",
+ "value": 0,
+ "pact:generator:type": "RandomDecimal",
+ "digits": 2
+ },
+ "string": {
+ "pact:matcher:type": "type",
+ "value": "",
+ "pact:generator:type": "RandomString"
+ },
+ "array": [
+ {
+ "pact:matcher:type": "number",
+ "value": 0,
+ "pact:generator:type": "RandomInt",
+ "min": 0,
+ "max": 10
+ },
+ {
+ "pact:matcher:type": "type",
+ "value": "",
+ "pact:generator:type": "RandomString"
+ }
+ ]
+ });
+ let response_body = CString::new(response_json.to_string()).unwrap();
+ let address = CString::new("127.0.0.1:0").unwrap();
+ let description = CString::new("a request to test the form urlencoded body").unwrap();
+ let method = CString::new("POST").unwrap();
+ let path = CString::new("/form-urlencoded").unwrap();
+ let content_type = CString::new("application/x-www-form-urlencoded").unwrap();
+ let status = 201;
+
+ pactffi_upon_receiving(interaction.clone(), description.as_ptr());
+ // with request...
+ pactffi_with_request(interaction.clone(), method.as_ptr(), path.as_ptr());
+ pactffi_with_header(interaction.clone(), InteractionPart::Request, accept_header.as_ptr(), 0, content_type.as_ptr());
+ pactffi_with_header(interaction.clone(), InteractionPart::Request, content_type_header.as_ptr(), 0, content_type.as_ptr());
+ pactffi_with_body(interaction.clone(), InteractionPart::Request, content_type.as_ptr(), body.as_ptr());
+ // will respond with...
+ pactffi_with_header(interaction.clone(), InteractionPart::Response, content_type_header.as_ptr(), 0, content_type.as_ptr());
+ pactffi_with_body(interaction.clone(), InteractionPart::Response, content_type.as_ptr(), response_body.as_ptr());
+ pactffi_response_status(interaction.clone(), status);
+ let port = pactffi_create_mock_server_for_pact(pact_handle.clone(), address.as_ptr(), false);
+
+ expect!(port).to(be_greater_than(0));
+
+ // Mock server has started, we can't now modify the pact
+ expect!(pactffi_upon_receiving(interaction.clone(), description.as_ptr())).to(be_false());
+
+ let client = Client::default();
+ let result = client.post(format!("http://127.0.0.1:{}/form-urlencoded", port).as_str())
+ .header("Accept", "application/x-www-form-urlencoded")
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .body("number=999.99&string=any+text&array=value2&array=value3")
+ .send();
+
+ match result {
+ Ok(res) => {
+ expect!(res.status()).to(be_eq(status));
+ expect!(res.headers().get("Content-Type").unwrap()).to(be_eq("application/x-www-form-urlencoded"));
+ expect!(res.text().unwrap()).to_not(be_equal_to("number=0&string=&array=0&array=".to_string()));
+ },
+ Err(_) => {
+ panic!("expected {} response but request failed", status);
+ }
+ };
+
+ let mismatches = unsafe {
+ CStr::from_ptr(pactffi_mock_server_mismatches(port)).to_string_lossy().into_owned()
+ };
+
+ pactffi_cleanup_mock_server(port);
+
+ expect!(mismatches).to(be_equal_to("[]"));
+}
diff --git a/rust/pact_matching/Cargo.toml b/rust/pact_matching/Cargo.toml
index ee5b4654f..8f1959a58 100644
--- a/rust/pact_matching/Cargo.toml
+++ b/rust/pact_matching/Cargo.toml
@@ -15,11 +15,12 @@ exclude = [
]
[features]
-default = ["datetime", "xml", "plugins", "multipart"]
+default = ["datetime", "xml", "plugins", "multipart", "form_urlencoded"]
datetime = ["pact_models/datetime", "pact-plugin-driver?/datetime", "dep:chrono"] # Support for date/time matchers and expressions
xml = ["pact_models/xml", "pact-plugin-driver?/xml", "dep:sxd-document"] # support for matching XML documents
plugins = ["dep:pact-plugin-driver"]
multipart = ["dep:multer"] # suport for MIME multipart bodies
+form_urlencoded = ["dep:serde_urlencoded"] # suport for matching form urlencoded
[dependencies]
ansi_term = "0.12.1"
@@ -41,14 +42,14 @@ mime = "0.3.17"
multer = { version = "3.0.0", features = ["all"], optional = true }
nom = "7.1.3"
onig = { version = "6.4.0", default-features = false }
-pact_models = { version = "~1.2.5", default-features = false }
+pact_models = { version = "~1.2.6", default-features = false }
pact-plugin-driver = { version = "~0.7.2", optional = true, default-features = false }
rand = "0.8.5"
reqwest = { version = "0.12.3", default-features = false, features = ["rustls-tls-native-roots", "json"] }
semver = "1.0.22"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
-serde_urlencoded = "0.7.1"
+serde_urlencoded = { version = "0.7.1", optional = true }
sxd-document = { version = "0.3.2", optional = true }
tokio = { version = "1.37.0", features = ["full"] }
tracing = "0.1.40"
diff --git a/rust/pact_matching/src/generators/bodies.rs b/rust/pact_matching/src/generators/bodies.rs
index 48c3cf19b..9cecf3983 100644
--- a/rust/pact_matching/src/generators/bodies.rs
+++ b/rust/pact_matching/src/generators/bodies.rs
@@ -9,6 +9,7 @@ use tracing::{debug, error, warn};
use pact_models::bodies::OptionalBody;
use pact_models::content_types::ContentType;
use pact_models::generators::{ContentTypeHandler, Generator, GeneratorTestMode, JsonHandler, VariantMatcher};
+use pact_models::generators::form_urlencoded::FormUrlEncodedHandler;
use pact_models::path_exp::DocPath;
use pact_models::plugins::PluginData;
#[cfg(feature = "xml")] use pact_models::xml_utils::parse_bytes;
@@ -67,6 +68,30 @@ pub async fn generators_process_body(
warn!("Generating XML documents requires the xml feature to be enabled");
Ok(body.clone())
}
+ } else if content_type.is_form_urlencoded() {
+ debug!("apply_body_generators: FORM URLENCODED content type");
+ #[cfg(feature = "form_urlencoded")]
+ {
+ let result: Result, serde_urlencoded::de::Error> = serde_urlencoded::from_bytes(&body.value().unwrap_or_default());
+ match result {
+ Ok(val) => {
+ let mut handler = FormUrlEncodedHandler { params: val };
+ Ok(handler.process_body(generators, mode, context, &matcher.boxed()).unwrap_or_else(|err| {
+ error!("Failed to generate the body: {}", err);
+ body.clone()
+ }))
+ },
+ Err(err) => {
+ error!("Failed to parse the body, so not applying any generators: {}", err);
+ Ok(body.clone())
+ }
+ }
+ }
+ #[cfg(not(feature = "form_urlencoded"))]
+ {
+ warn!("Generating FORM URLENCODED query string requires the form_urlencoded feature to be enabled");
+ Ok(body.clone())
+ }
}
else {
#[cfg(feature = "plugins")]