Skip to content

Commit a56efcf

Browse files
Copilotmefellows
andauthored
fix: support ProviderStateGenerator shorthand expression format for query, header, and body (FFI)
When a JSON object contains an `expression` key (without `pact:matcher:type`), it is now treated as a ProviderStateGenerator shorthand. The `value` field is extracted as the example value, and a ProviderStateGenerator is configured with the expression. This fixes the issue where `{"expression":"${var}","value":"100"}` was not working for query parameters, headers, and bodies - only the path case worked before. Agent-Logs-Url: https://github.com/pact-foundation/pact-reference/sessions/fb2a6dd6-ef83-46ac-af88-f8edd5445fc0 Co-authored-by: mefellows <53900+mefellows@users.noreply.github.com>
1 parent d64f674 commit a56efcf

3 files changed

Lines changed: 213 additions & 0 deletions

File tree

rust/pact_ffi/IntegrationJson.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,40 @@ we can use the JSON format with a regex matcher (which requires a "regex" attrib
3030
pactffi_with_request(handle, "PUT", "{\"value\": \"/path/to/100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\/path\\\\/to\\\\/\\\\d+\"}")
3131
```
3232

33+
## Using ProviderState generators
34+
35+
The `ProviderState` generator allows values to be sourced from provider state parameters during provider verification.
36+
There are two formats for specifying a `ProviderState` generator:
37+
38+
### Shorthand format (expression only)
39+
40+
When you only need a `ProviderState` generator (without any matching rule), you can use the shorthand format with just
41+
the `expression` and `value` keys:
42+
43+
```json
44+
{
45+
"expression": "${accountNumber}",
46+
"value": "100"
47+
}
48+
```
49+
50+
This sets up a `ProviderStateGenerator` with the given expression and uses the `value` as the example value. This
51+
shorthand is supported for query parameters, headers, path, and request/response bodies.
52+
53+
### Full format (with matcher)
54+
55+
To combine a `ProviderState` generator with a matching rule, use the full format with `pact:generator:type` and
56+
`pact:matcher:type`:
57+
58+
```json
59+
{
60+
"value": "100",
61+
"pact:matcher:type": "type",
62+
"pact:generator:type": "ProviderState",
63+
"expression": "${accountNumber}"
64+
}
65+
```
66+
3367
## Matching on Paths
3468

3569
function: `pactffi_with_request`

rust/pact_ffi/src/mock_server/bodies.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ pub fn process_object(
6767
let result = if let Some(matcher_type) = obj.get("pact:matcher:type") {
6868
debug!("detected pact:matcher:type, will configure a matcher");
6969
process_matcher(obj, matching_rules, generators, &path, type_matcher, &matcher_type.clone())
70+
} else if obj.contains_key("expression") && !obj.contains_key("pact:matcher:type") {
71+
debug!("detected 'expression' without 'pact:matcher:type', configuring ProviderStateGenerator");
72+
if let Some(generator) = Generator::from_map("ProviderState", obj) {
73+
let category = generator_category(matching_rules);
74+
generators.add_generator_with_subcategory(category, path.clone(), generator);
75+
}
76+
obj.get("value").cloned().unwrap_or(Value::Null)
7077
} else {
7178
debug!("Configuring a normal object");
7279
Value::Object(obj.iter()
@@ -461,6 +468,57 @@ use pretty_assertions::assert_eq;
461468
expect!(result).to(be_equal_to(json));
462469
}
463470

471+
// Issue #460 - ProviderStateGenerator shorthand with expression key (no pact:matcher:type)
472+
#[test_log::test]
473+
fn process_object_with_expression_provider_state_generator() {
474+
// A body where accountNumber uses the expression shorthand
475+
let json = json!({
476+
"accountNumber": {
477+
"expression": "${accountNumber}",
478+
"value": 100
479+
}
480+
});
481+
let mut matching_rules = MatchingRuleCategory::empty("body");
482+
let mut generators = Generators::default();
483+
let result = process_object(json.as_object().unwrap(), &mut matching_rules,
484+
&mut generators, DocPath::root(), false);
485+
486+
// The value should be extracted, not the full object
487+
expect!(result).to(be_equal_to(json!({"accountNumber": 100})));
488+
// No matching rules should be added (no pact:matcher:type)
489+
expect!(matching_rules.is_empty()).to(be_true());
490+
// A ProviderStateGenerator should be configured
491+
expect!(generators).to(be_equal_to(generators! {
492+
"BODY" => {
493+
"$.accountNumber" => Generator::ProviderStateGenerator("${accountNumber}".to_string(), None)
494+
}
495+
}));
496+
}
497+
498+
// Issue #460 - ProviderStateGenerator shorthand with string value
499+
#[test_log::test]
500+
fn process_object_with_expression_provider_state_generator_string_value() {
501+
let json = json!({
502+
"expression": "${accountNumber}",
503+
"value": "100"
504+
});
505+
let mut matching_rules = MatchingRuleCategory::empty("body");
506+
let mut generators = Generators::default();
507+
let result = process_object(json.as_object().unwrap(), &mut matching_rules,
508+
&mut generators, DocPath::root(), false);
509+
510+
// The value should be extracted
511+
expect!(result).to(be_equal_to(json!("100")));
512+
// No matching rules
513+
expect!(matching_rules.is_empty()).to(be_true());
514+
// A ProviderStateGenerator should be configured at root
515+
expect!(generators).to(be_equal_to(generators! {
516+
"BODY" => {
517+
"$" => Generator::ProviderStateGenerator("${accountNumber}".to_string(), None)
518+
}
519+
}));
520+
}
521+
464522
#[test]
465523
fn process_object_with_matching_rule_test() {
466524
let json = json!({

rust/pact_ffi/src/mock_server/handles.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,18 @@ fn from_integration_json_v2(
10691069
}
10701070

10711071
result_value
1072+
} else if map.contains_key("expression") {
1073+
debug!("detected 'expression' without 'pact:matcher:type', configuring ProviderStateGenerator");
1074+
if let Some(generator) = Generator::from_map("ProviderState", map) {
1075+
let category = generator_category(matching_rules);
1076+
let gen_path = if path_or_status {
1077+
path.parent().unwrap_or(DocPath::root())
1078+
} else {
1079+
path.clone()
1080+
};
1081+
generators.add_generator_with_subcategory(category, gen_path, generator);
1082+
}
1083+
map.get("value").cloned().unwrap_or_default()
10721084
} else {
10731085
debug!("Configuring a normal value using the 'value' attribute");
10741086
map.get("value").cloned().unwrap_or_default()
@@ -3488,6 +3500,82 @@ mod tests {
34883500
.to(be_equal_to(Either::Right(vec!["100".to_string(), "200".to_string()])));
34893501
}
34903502

3503+
// Issue #460 - ProviderStateGenerator shorthand with expression key (no pact:matcher:type)
3504+
#[test_log::test]
3505+
fn from_integration_json_expression_provider_state_generator() {
3506+
let mut rules = MatchingRules::default();
3507+
let mut generators = Generators::default();
3508+
let mut path = DocPath::root();
3509+
path.push_field("accountNumber");
3510+
3511+
let result = from_integration_json_v2(
3512+
&mut rules, &mut generators,
3513+
r#"{"expression":"${accountNumber}","value":"100"}"#,
3514+
path.clone(), "query", 0
3515+
);
3516+
3517+
expect!(result).to(be_equal_to(Either::Left("100".to_string())));
3518+
let gen_cat = generators.categories.get(&pact_models::generators::GeneratorCategory::QUERY);
3519+
assert!(gen_cat.is_some(), "Expected QUERY generator category to be present");
3520+
let gen_path = DocPath::root().join("accountNumber");
3521+
let gen = gen_cat.unwrap().get(&gen_path);
3522+
assert_eq!(gen, Some(&pact_models::generators::Generator::ProviderStateGenerator("${accountNumber}".to_string(), None)));
3523+
}
3524+
3525+
// Issue #460 - query parameter with ProviderStateGenerator shorthand (no pact:matcher:type)
3526+
#[test_log::test]
3527+
fn query_with_provider_state_generator_shorthand() {
3528+
let pact_handle = PactHandle::new("TestQPSG1", "TestQPSGP1");
3529+
let description = CString::new("query_with_provider_state_generator_shorthand").unwrap();
3530+
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
3531+
3532+
let name = CString::new("accountNumber").unwrap();
3533+
let value = CString::new(r#"{"expression":"${accountNumber}","value":"100"}"#).unwrap();
3534+
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
3535+
3536+
let interaction = handle.with_interaction(&|_, _, inner| {
3537+
inner.as_v4_http().unwrap()
3538+
}).unwrap();
3539+
3540+
pactffi_free_pact_handle(pact_handle);
3541+
3542+
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
3543+
"accountNumber".to_string() => vec![Some("100".to_string())]
3544+
}));
3545+
expect!(&interaction.request.generators).to(be_equal_to(&generators! {
3546+
"query" => {
3547+
"$.accountNumber" => pact_models::generators::Generator::ProviderStateGenerator("${accountNumber}".to_string(), None)
3548+
}
3549+
}));
3550+
}
3551+
3552+
// Issue #460 - header with ProviderStateGenerator shorthand (no pact:matcher:type)
3553+
#[test_log::test]
3554+
fn header_with_provider_state_generator_shorthand() {
3555+
let pact_handle = PactHandle::new("TestHPSG1", "TestHPSGP1");
3556+
let description = CString::new("header_with_provider_state_generator_shorthand").unwrap();
3557+
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
3558+
3559+
let name = CString::new("x-account-id").unwrap();
3560+
let value = CString::new(r#"{"expression":"${accountId}","value":"ABC123"}"#).unwrap();
3561+
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 0, value.as_ptr());
3562+
3563+
let interaction = handle.with_interaction(&|_, _, inner| {
3564+
inner.as_v4_http().unwrap()
3565+
}).unwrap();
3566+
3567+
pactffi_free_pact_handle(pact_handle);
3568+
3569+
expect!(interaction.request.headers.clone()).to(be_some().value(hashmap!{
3570+
"x-account-id".to_string() => vec!["ABC123".to_string()]
3571+
}));
3572+
expect!(&interaction.request.generators).to(be_equal_to(&generators! {
3573+
"header" => {
3574+
"$['x-account-id']" => pact_models::generators::Generator::ProviderStateGenerator("${accountId}".to_string(), None)
3575+
}
3576+
}));
3577+
}
3578+
34913579
#[test]
34923580
fn pactffi_with_metadata_async() {
34933581
let pact_handle = PactHandle::new("metadata-consumer", "metadata-provider");
@@ -4604,4 +4692,37 @@ mod tests {
46044692
expect!(headers.get("Content-Type").unwrap().first().unwrap())
46054693
.to(be_equal_to(&JSON.to_string()));
46064694
}
4695+
4696+
// Issue #460 - body with ProviderStateGenerator shorthand (expression key, no pact:matcher:type)
4697+
#[test_log::test]
4698+
fn body_with_provider_state_generator_shorthand() {
4699+
let pact_handle = PactHandle::new("TestBPSG1", "TestBPSGP1");
4700+
let description = CString::new("body_with_provider_state_generator_shorthand").unwrap();
4701+
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
4702+
4703+
let json_ct = CString::new(JSON.to_string()).unwrap();
4704+
// Body where accountNumber field uses the expression shorthand for ProviderStateGenerator
4705+
let body = CString::new(r#"{"accountNumber":{"expression":"${accountNumber}","value":100}}"#).unwrap();
4706+
let result = pactffi_with_body(i_handle, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
4707+
4708+
let interaction = i_handle
4709+
.with_interaction(&|_, _, inner| inner.as_v4_http().unwrap())
4710+
.unwrap();
4711+
4712+
pactffi_free_pact_handle(pact_handle);
4713+
4714+
expect!(result).to(be_true());
4715+
4716+
// The body should have the value extracted (100), not the full expression object
4717+
let body_bytes = interaction.request.body.value().unwrap();
4718+
let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
4719+
assert_eq!(body_json, json!({"accountNumber": 100}));
4720+
4721+
// A ProviderStateGenerator should be configured for the accountNumber field
4722+
expect!(&interaction.request.generators).to(be_equal_to(&generators! {
4723+
"body" => {
4724+
"$.accountNumber" => pact_models::generators::Generator::ProviderStateGenerator("${accountNumber}".to_string(), None)
4725+
}
4726+
}));
4727+
}
46074728
}

0 commit comments

Comments
 (0)