Skip to main content

pact_models/
request.rs

1//! Structs to model an HTTP request
2
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::hash::{Hash, Hasher};
6use std::str::from_utf8;
7
8use base64::Engine;
9use base64::engine::general_purpose::STANDARD as BASE64;
10use itertools::Itertools;
11use maplit::hashmap;
12use serde_json::{json, Value};
13use tracing::warn;
14
15use crate::{DifferenceType, PactSpecification};
16use crate::bodies::OptionalBody;
17use crate::generators::{Generators, generators_from_json, generators_to_json};
18use crate::http_parts::HttpPart;
19use crate::json_utils::{body_from_json, headers_from_json, headers_to_json};
20use crate::matchingrules::{matchers_from_json, matchers_to_json, MatchingRules};
21use crate::query_strings::{query_from_json, query_to_json, v3_query_from_json};
22use crate::v4::http_parts::HttpRequest;
23
24/// Struct that defines the request.
25#[derive(Debug, Clone, Eq)]
26pub struct Request {
27  /// Request method
28  pub method: String,
29  /// Request path
30  pub path: String,
31  /// Request query string
32  pub query: Option<HashMap<String, Vec<Option<String>>>>,
33  /// Request headers
34  pub headers: Option<HashMap<String, Vec<String>>>,
35  /// Request body
36  pub body: OptionalBody,
37  /// Request matching rules
38  pub matching_rules: MatchingRules,
39  /// Request generators
40  pub generators: Generators
41}
42
43impl HttpPart for Request {
44  fn headers(&self) -> &Option<HashMap<String, Vec<String>>> {
45    &self.headers
46  }
47
48  fn headers_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
49    if self.headers.is_none() {
50      self.headers = Some(hashmap!{});
51    }
52    self.headers.as_mut().unwrap()
53  }
54
55  fn body(&self) -> &OptionalBody {
56    &self.body
57  }
58
59  fn body_mut(&mut self) -> &mut OptionalBody {
60    &mut self.body
61  }
62
63  fn matching_rules(&self) -> &MatchingRules {
64    &self.matching_rules
65  }
66
67  fn matching_rules_mut(&mut self) -> &mut MatchingRules {
68    &mut self.matching_rules
69  }
70
71  fn generators(&self) -> &Generators {
72    &self.generators
73  }
74
75  fn generators_mut(&mut self) -> &mut Generators {
76    &mut self.generators
77  }
78
79  fn lookup_content_type(&self) -> Option<String> {
80    self.lookup_header_value(&"content-type".to_string())
81  }
82}
83
84impl Hash for Request {
85  fn hash<H: Hasher>(&self, state: &mut H) {
86    self.method.hash(state);
87    self.path.hash(state);
88    if let Some(query) = &self.query {
89      for (k, v) in query.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
90        k.hash(state);
91        v.hash(state);
92      }
93    }
94    if let Some(headers) = &self.headers {
95      for (k, v) in headers.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
96        k.hash(state);
97        v.hash(state);
98      }
99    }
100    self.body.hash(state);
101    self.matching_rules.hash(state);
102    self.generators.hash(state);
103  }
104}
105
106impl PartialEq for Request {
107  fn eq(&self, other: &Self) -> bool {
108    self.method == other.method && self.path == other.path && self.query == other.query &&
109      self.headers == other.headers && self.body == other.body &&
110      self.matching_rules == other.matching_rules && self.generators == other.generators
111  }
112
113  fn ne(&self, other: &Self) -> bool {
114    self.method != other.method || self.path != other.path || self.query != other.query ||
115      self.headers != other.headers || self.body != other.body ||
116      self.matching_rules != other.matching_rules || self.generators != other.generators
117  }
118}
119
120impl Display for Request {
121  fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
122    write!(f, "Request ( method: {}, path: {}, query: {:?}, headers: {:?}, body: {} )",
123           self.method, self.path, self.query, self.headers, self.body)
124  }
125}
126
127impl Default for Request {
128  fn default() -> Self {
129    Request {
130      method: "GET".to_string(),
131      path: "/".to_string(),
132      query: None,
133      headers: None,
134      body: OptionalBody::Missing,
135      matching_rules: MatchingRules::default(),
136      generators: Generators::default()
137    }
138  }
139}
140
141impl Request {
142  /// Builds a `Request` from a `Value` struct.
143  pub fn from_json(request_json: &Value, spec_version: &PactSpecification
144  ) -> anyhow::Result<Request> {
145    let method_val = match request_json.get("method") {
146      Some(v) => match *v {
147        Value::String(ref s) => s.to_uppercase(),
148        _ => v.to_string().to_uppercase()
149      },
150      None => "GET".to_string()
151    };
152    let path_val = match request_json.get("path") {
153      Some(v) => match *v {
154        Value::String(ref s) => s.clone(),
155        _ => v.to_string()
156      },
157      None => "/".to_string()
158    };
159    let query_val = match request_json.get("query") {
160      Some(v) => match spec_version {
161        &PactSpecification::V3 => v3_query_from_json(v, spec_version),
162        _ => query_from_json(v, spec_version)
163      },
164      None => None
165    };
166    let headers = headers_from_json(request_json);
167    Ok(Request {
168      method: method_val,
169      path: path_val,
170      query: query_val,
171      headers: headers.clone(),
172      body: body_from_json(request_json, "body", &headers),
173      matching_rules: matchers_from_json(request_json, &Some("requestMatchingRules".to_string()))?,
174      generators: generators_from_json(request_json)?,
175    })
176  }
177
178  /// Converts this `Request` to a `Value` struct.
179  pub fn to_json(&self, spec_version: &PactSpecification) -> Value {
180    let mut json = json!({
181            "method".to_string() : Value::String(self.method.to_uppercase()),
182            "path".to_string() : Value::String(self.path.clone())
183        });
184    {
185      let map = json.as_object_mut().unwrap();
186
187      if self.query.is_some() {
188        map.insert("query".to_string(), query_to_json(self.query.clone().unwrap(), spec_version));
189      }
190
191      if self.headers.is_some() {
192        map.insert("headers".to_string(), headers_to_json(&self.headers.clone().unwrap()));
193      }
194
195      match self.body {
196        OptionalBody::Present(ref body, _, _) => if self.content_type().unwrap_or_default().is_json() {
197          match serde_json::from_slice(body) {
198            Ok(json_body) => { map.insert("body".to_string(), json_body); },
199            Err(err) => {
200              warn!("Failed to parse json body: {}", err);
201              map.insert("body".to_string(), Value::String(BASE64.encode(body)));
202            }
203          }
204        } else {
205          match from_utf8(body) {
206            Ok(s) => map.insert("body".to_string(), Value::String(s.to_string())),
207            Err(_) => map.insert("body".to_string(), Value::String(BASE64.encode(body)))
208          };
209        },
210        OptionalBody::Empty => { map.insert("body".to_string(), Value::String(String::default())); },
211        OptionalBody::Missing => (),
212        OptionalBody::Null => { map.insert("body".to_string(), Value::Null); }
213      }
214
215      if self.matching_rules.is_not_empty() {
216        map.insert("matchingRules".to_string(), matchers_to_json(
217          &self.matching_rules.clone(), spec_version));
218      }
219
220      if self.generators.is_not_empty() {
221        map.insert("generators".to_string(), generators_to_json(
222          &self.generators.clone(), spec_version));
223      }
224    }
225    json
226  }
227
228  /// Returns the default request: a GET request to the root.
229  #[deprecated(since="0.6.0", note="please use `default()` from the standard Default trait instead")]
230  pub fn default_request() -> Request {
231    Request::default()
232  }
233
234  /// Return a description of all the differences from the other request
235  pub fn differences_from(&self, other: &Request) -> Vec<(DifferenceType, String)> {
236    let mut differences = vec![];
237    if self.method != other.method {
238      differences.push((DifferenceType::Method, format!("Request method {} != {}", self.method, other.method)));
239    }
240    if self.path != other.path {
241      differences.push((DifferenceType::Path, format!("Request path {} != {}", self.path, other.path)));
242    }
243    if self.query != other.query {
244      differences.push((DifferenceType::QueryParameters, format!("Request query {:?} != {:?}", self.query, other.query)));
245    }
246    let mut keys = self.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
247    let mut other_keys = other.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
248    keys.sort();
249    other_keys.sort();
250    if keys != other_keys {
251      differences.push((DifferenceType::Headers, format!("Request headers {:?} != {:?}", self.headers, other.headers)));
252    }
253    if self.body != other.body {
254      differences.push((DifferenceType::Body, format!("Request body '{:?}' != '{:?}'", self.body, other.body)));
255    }
256    if self.matching_rules != other.matching_rules {
257      differences.push((DifferenceType::MatchingRules, format!("Request matching rules {:?} != {:?}", self.matching_rules, other.matching_rules)));
258    }
259    differences
260  }
261
262  /// Convert this interaction to V4 format
263  pub fn as_v4_request(&self) -> HttpRequest {
264    HttpRequest {
265      method: self.method.clone(),
266      path: self.path.clone(),
267      query: self.query.clone(),
268      headers: self.headers.clone(),
269      body: self.body.clone(),
270      matching_rules: self.matching_rules.clone(),
271      generators: self.generators.clone()
272    }
273  }
274}
275
276#[cfg(test)]
277mod tests {
278  use std::collections::hash_map::DefaultHasher;
279  use std::hash::{Hash, Hasher};
280
281  use expectest::prelude::*;
282  use maplit::hashmap;
283  use serde_json::Value;
284
285  use crate::bodies::OptionalBody;
286  use crate::content_types::{HTML, JSON, XML};
287  use crate::generators::{Generator, GeneratorCategory, Generators};
288  use crate::http_parts::HttpPart;
289  use crate::matchingrules::{Category, MatchingRule, MatchingRuleCategory, MatchingRules, RuleList, RuleLogic};
290  use crate::PactSpecification;
291  use crate::path_exp::DocPath;
292  use crate::request::Request;
293
294  #[test]
295  fn request_from_json_defaults_to_get() {
296    let request_json : serde_json::Value = serde_json::from_str(r#"
297      {
298          "path": "/",
299          "query": "",
300          "headers": {}
301      }
302     "#).unwrap();
303    let request = Request::from_json(&request_json, &PactSpecification::V1);
304    expect!(request.unwrap().method).to(be_equal_to("GET"));
305  }
306
307  #[test]
308  fn request_from_json_defaults_to_root_for_path() {
309    let request_json : serde_json::Value = serde_json::from_str(r#"
310      {
311          "method": "PUT",
312          "query": "",
313          "headers": {}
314      }
315     "#).unwrap();
316    println!("request_json: {}", request_json);
317    let request = Request::from_json(&request_json, &PactSpecification::V1_1);
318    assert_eq!(request.unwrap().path, "/".to_string());
319  }
320
321  #[test]
322  fn request_content_type_is_based_on_the_content_type_header() {
323    let request = Request {
324      method: "GET".to_string(),
325      path: "/".to_string(),
326      query: None,
327      headers: None,
328      body: OptionalBody::Missing,
329      ..Request::default()
330    };
331    expect!(request.content_type().unwrap_or_default().to_string()).to(be_equal_to("*/*"));
332    expect!(Request {
333        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
334      .to(be_equal_to("text/html"));
335    expect!(Request {
336        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
337      .to(be_equal_to("application/json;charset=utf-8"));
338    expect!(Request {
339        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
340      .to(be_equal_to("application/json"));
341    expect!(Request {
342        headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
343      .to(be_equal_to("application/json;charset=utf-8"));
344    expect!(Request {
345        body: OptionalBody::Present("{\"json\": true}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
346      .to(be_equal_to("application/json"));
347    expect!(Request {
348        body: OptionalBody::Present("{}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
349      .to(be_equal_to("application/json"));
350    expect!(Request {
351        body: OptionalBody::Present("[]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
352      .to(be_equal_to("application/json"));
353    expect!(Request {
354        body: OptionalBody::Present("[1,2,3]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
355      .to(be_equal_to("application/json"));
356    expect!(Request {
357        body: OptionalBody::Present("\"string\"".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
358      .to(be_equal_to("application/json"));
359    expect!(Request {
360        body: OptionalBody::Present("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<json>false</json>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
361      .to(be_equal_to("application/xml"));
362    expect!(Request {
363        body: OptionalBody::Present("<json>false</json>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
364      .to(be_equal_to("application/xml"));
365    expect!(Request {
366        body: OptionalBody::Present("this is not json".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
367      .to(be_equal_to("text/plain"));
368    expect!(Request {
369        body: OptionalBody::Present("<html><body>this is also not json</body></html>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
370      .to(be_equal_to("text/html"));
371  }
372
373  #[test]
374  fn content_type_struct_test() {
375    let request = Request {
376      method: "GET".to_string(),
377      path: "/".to_string(),
378      query: None,
379      headers: None,
380      body: OptionalBody::Missing,
381      ..Request::default()
382    };
383    expect!(request.content_type()).to(be_none());
384    expect!(Request {
385        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type())
386      .to(be_some().value(HTML.clone()));
387    expect!(Request {
388        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type())
389      .to(be_some().value(JSON.clone()));
390    expect!(Request {
391        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/hal+json".to_string()] }), .. request.clone() }
392        .content_type().map(|c| c.base_type()))
393      .to(be_some().value(JSON.clone()));
394    expect!(Request {
395        headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/xml".to_string()] }), .. request.clone() }.content_type())
396      .to(be_some().value(XML.clone()));
397    expect!(Request {
398        headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/stuff+xml".to_string()] }), ..
399        request.clone() }.content_type().map(|c| c.base_type()))
400      .to(be_some().value(XML.clone()));
401  }
402
403  #[test]
404  fn request_to_json_with_defaults() {
405    let request = Request::default();
406    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
407      be_equal_to("{\"method\":\"GET\",\"path\":\"/\"}"));
408  }
409
410  #[test]
411  fn request_to_json_converts_methods_to_upper_case() {
412    let request = Request { method: "post".to_string(), .. Request::default() };
413    expect!(request.to_json(&PactSpecification::V3).to_string()).to(be_equal_to("{\"method\":\"POST\",\"path\":\"/\"}"));
414  }
415
416  #[test]
417  fn request_to_json_with_a_query() {
418    let request = Request { query: Some(hashmap!{
419        "a".to_string() => vec![Some("1".to_string()), Some("2".to_string())],
420        "b".to_string() => vec![Some("3".to_string())]
421    }), .. Request::default() };
422    expect!(request.to_json(&PactSpecification::V2).to_string()).to(
423      be_equal_to(r#"{"method":"GET","path":"/","query":"a=1&a=2&b=3"}"#)
424    );
425  }
426
427  #[test]
428  fn request_to_json_with_a_query_must_encode_the_query() {
429    let request = Request { query: Some(hashmap!{
430        "datetime".to_string() => vec![Some("2011-12-03T10:15:30+01:00".to_string())],
431        "description".to_string() => vec![Some("hello world!".to_string())] }), .. Request::default() };
432    expect!(request.to_json(&PactSpecification::V2).to_string()).to(
433      be_equal_to(r#"{"method":"GET","path":"/","query":"datetime=2011-12-03T10%3a15%3a30%2b01%3a00&description=hello+world%21"}"#)
434    );
435  }
436
437  #[test]
438  fn request_to_json_with_a_query_must_encode_the_query_with_utf8_chars() {
439    let request = Request { query: Some(hashmap!{
440        "a".to_string() => vec![Some("b=c&d❤".to_string())]
441    }), .. Request::default() };
442    expect!(request.to_json(&PactSpecification::V2).to_string()).to(
443      be_equal_to(r#"{"method":"GET","path":"/","query":"a=b%3dc%26d%27%64"}"#)
444    );
445  }
446
447  #[test]
448  fn request_to_json_with_a_query_v3() {
449    let request = Request { query: Some(hashmap!{
450        "a".to_string() => vec![Some("1".to_string()), Some("2".to_string())],
451        "b".to_string() => vec![Some("3".to_string())]
452    }), .. Request::default() };
453    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
454      be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["1","2"],"b":["3"]}}"#)
455    );
456  }
457
458  #[test]
459  fn request_to_json_with_a_query_v3_must_not_encode_the_query() {
460    let request = Request { query: Some(hashmap!{
461        "datetime".to_string() => vec![Some("2011-12-03T10:15:30+01:00".to_string())],
462        "description".to_string() => vec![Some("hello world!".to_string())] }), .. Request::default() };
463    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
464      be_equal_to(r#"{"method":"GET","path":"/","query":{"datetime":["2011-12-03T10:15:30+01:00"],"description":["hello world!"]}}"#)
465    );
466  }
467
468  #[test]
469  fn request_to_json_with_a_query_v3_must_not_encode_the_query_with_utf8_chars() {
470    let request = Request { query: Some(hashmap!{
471        "a".to_string() => vec![Some("b=c&d❤".to_string())]
472    }), .. Request::default() };
473    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
474      be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["b=c&d❤"]}}"#)
475    );
476  }
477
478  #[test]
479  fn request_to_json_with_headers() {
480    let request = Request { headers: Some(hashmap!{
481        "HEADERA".to_string() => vec!["VALUEA".to_string()],
482        "HEADERB".to_string() => vec!["VALUEB1, VALUEB2".to_string()]
483    }), .. Request::default() };
484    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
485      be_equal_to(r#"{"headers":{"HEADERA":"VALUEA","HEADERB":"VALUEB1, VALUEB2"},"method":"GET","path":"/"}"#)
486    );
487  }
488
489  #[test]
490  fn request_to_json_with_json_body() {
491    let request = Request { headers: Some(hashmap!{
492        "Content-Type".to_string() => vec!["application/json".to_string()]
493    }), body: OptionalBody::Present(r#"{"key": "value"}"#.into(), None, None), .. Request::default() };
494    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
495      be_equal_to(r#"{"body":{"key":"value"},"headers":{"Content-Type":"application/json"},"method":"GET","path":"/"}"#)
496    );
497  }
498
499
500  #[test]
501  fn request_to_json_with_non_json_body() {
502    let request = Request { headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/plain".to_string()] }),
503      body: OptionalBody::Present("This is some text".into(), None, None), .. Request::default() };
504    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
505      be_equal_to(r#"{"body":"This is some text","headers":{"Content-Type":"text/plain"},"method":"GET","path":"/"}"#)
506    );
507  }
508
509  #[test]
510  fn request_to_json_with_empty_body() {
511    let request = Request { body: OptionalBody::Empty, .. Request::default() };
512    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
513      be_equal_to(r#"{"body":"","method":"GET","path":"/"}"#)
514    );
515  }
516
517  #[test]
518  fn request_to_json_with_null_body() {
519    let request = Request { body: OptionalBody::Null, .. Request::default() };
520    expect!(request.to_json(&PactSpecification::V3).to_string()).to(
521      be_equal_to(r#"{"body":null,"method":"GET","path":"/"}"#)
522    );
523  }
524
525  fn hash<T: Hash>(t: &T) -> u64 {
526    let mut s = DefaultHasher::new();
527    t.hash(&mut s);
528    s.finish()
529  }
530
531  #[test]
532  fn hash_for_request() {
533    let request1 = Request::default();
534    let request2 = Request { method: "POST".to_string(), .. Request::default() };
535    let request3 = Request { headers: Some(hashmap!{
536        "H1".to_string() => vec!["A".to_string()]
537    }), .. Request::default() };
538    let request4 = Request { headers: Some(hashmap!{
539        "H1".to_string() => vec!["B".to_string()]
540    }), .. Request::default() };
541    expect!(hash(&request1)).to(be_equal_to(hash(&request1)));
542    expect!(hash(&request3)).to(be_equal_to(hash(&request3)));
543    expect!(hash(&request1)).to_not(be_equal_to(hash(&request2)));
544    expect!(hash(&request3)).to_not(be_equal_to(hash(&request4)));
545  }
546
547  #[test]
548  fn request_headers_do_not_conflict_if_they_have_been_serialised_and_deserialised_to_json() {
549    // headers are serialised in a hashmap; serializing and deserializing can can change the
550    // internal order of the keys in the hashmap, and this can confuse the differences_from code.
551    let original_request = Request {
552      method: "".to_string(),
553      path: "".to_string(),
554      query: None,
555      headers: Some(hashmap! {
556          "accept".to_string() => vec!["application/xml".to_string(), "application/json".to_string()],
557          "user-agent".to_string() => vec!["test".to_string(), "test2".to_string()],
558          "content-type".to_string() => vec!["text/plain".to_string()]
559        }),
560      body: OptionalBody::Missing,
561      matching_rules: Default::default(),
562      generators: Default::default(),
563    };
564
565    let json = serde_json::to_string(&original_request
566      .to_json(&PactSpecification::V3)).expect("could not serialize");
567
568    let serialized_and_deserialized_request: Value =
569      serde_json::from_str(&json).expect("could not deserialize");
570
571    expect!(original_request
572        .differences_from(&Request::from_json(&serialized_and_deserialized_request, &PactSpecification::V3).unwrap())
573        .iter())
574      .to(be_empty());
575  }
576
577  // Issue https://github.com/pact-foundation/pact-js-core/issues/400
578  #[test]
579  fn to_json_with_provider_state_generator_test() {
580    let request = Request {
581      method: "GET".to_string(),
582      path: "/data/42".to_string(),
583      matching_rules: MatchingRules {
584        rules: hashmap!{
585          Category::PATH => MatchingRuleCategory {
586            name: Category::PATH,
587            rules: hashmap!{
588              DocPath::root() => RuleList {
589                rules: vec![MatchingRule::Type],
590                rule_logic: RuleLogic::And,
591                cascaded: false
592              }
593            }
594          }
595        }
596      },
597      generators: Generators {
598        categories: hashmap!{
599          GeneratorCategory::PATH => hashmap!{
600            DocPath::root() => Generator::ProviderStateGenerator("/data/${id}".to_string(), None)
601          }
602        }
603      },
604      .. Request::default()
605    };
606
607    let json = request.to_json(&PactSpecification::V3);
608
609    let generators = json.get("generators").unwrap();
610    expect!(generators.to_string()).to_not(be_equal_to("{}"));
611
612    let rules = json.get("matchingRules").unwrap();
613    expect!(rules.to_string()).to_not(be_equal_to("{}"));
614  }
615}