pact_models/v4/
sync_message.rs

1//! Synchronous interactions as a request message to a sequence of response messages
2
3use std::collections::hash_map::DefaultHasher;
4use std::collections::HashMap;
5use std::fmt::{Display, Formatter};
6use std::hash::{Hash, Hasher};
7use std::panic::RefUnwindSafe;
8use std::sync::{Arc, Mutex};
9
10use anyhow::anyhow;
11use itertools::Itertools;
12use serde_json::{json, Map, Value};
13use tracing::warn;
14
15use crate::bodies::OptionalBody;
16use crate::content_types::ContentType;
17use crate::interaction::Interaction;
18use crate::json_utils::{is_empty, json_to_string};
19use crate::matchingrules::MatchingRules;
20use crate::message::Message;
21use crate::provider_states::ProviderState;
22use crate::sync_interaction::RequestResponseInteraction;
23use crate::v4::async_message::AsynchronousMessage;
24use crate::v4::interaction::{InteractionMarkup, parse_plugin_config, V4Interaction};
25use crate::v4::message_parts::MessageContents;
26use crate::v4::synch_http::SynchronousHttp;
27use crate::v4::V4InteractionType;
28
29/// Synchronous interactions as a request message to a sequence of response messages
30#[derive(Debug, Clone, Eq)]
31pub struct SynchronousMessage {
32  /// Interaction ID. This will only be set if the Pact file was fetched from a Pact Broker
33  pub id: Option<String>,
34  /// Unique key for this interaction
35  pub key: Option<String>,
36  /// A description for the interaction. Must be unique within the Pact file
37  pub description: String,
38  /// Optional provider state for the interaction.
39  /// See https://docs.pact.io/getting_started/provider_states for more info on provider states.
40  pub provider_states: Vec<ProviderState>,
41  /// Annotations and comments associated with this interaction
42  pub comments: HashMap<String, Value>,
43  /// Request message
44  pub request: MessageContents,
45  /// Response messages
46  pub response: Vec<MessageContents>,
47
48  /// If this interaction is pending. Pending interactions will never fail the build if they fail
49  pub pending: bool,
50
51  /// Configuration added by plugins
52  pub plugin_config: HashMap<String, HashMap<String, Value>>,
53
54  /// Text markup to use to render the interaction in a UI
55  pub interaction_markup: InteractionMarkup,
56
57  /// Transport mechanism used with this message
58  pub transport: Option<String>
59}
60
61impl SynchronousMessage {
62  fn calc_hash(&self) -> String {
63    let mut s = DefaultHasher::new();
64    self.hash(&mut s);
65    format!("{:x}", s.finish())
66  }
67
68  /// Creates a new version with a calculated key
69  pub fn with_key(&self) -> SynchronousMessage {
70    SynchronousMessage {
71      key: Some(self.calc_hash()),
72      .. self.clone()
73    }
74  }
75
76  /// Parse the JSON into a SynchronousMessages structure
77  pub fn from_json(json: &Value, index: usize) -> anyhow::Result<SynchronousMessage> {
78    if json.is_object() {
79      let id = json.get("_id").map(|id| json_to_string(id));
80      let key = json.get("key").map(|id| json_to_string(id));
81      let description = match json.get("description") {
82        Some(v) => match *v {
83          Value::String(ref s) => s.clone(),
84          _ => v.to_string()
85        },
86        None => format!("Interaction {}", index)
87      };
88
89      let comments = match json.get("comments") {
90        Some(v) => match v {
91          Value::Object(map) => map.iter()
92            .map(|(k, v)| (k.clone(), v.clone())).collect(),
93          _ => {
94            warn!("Interaction comments must be a JSON Object, but received {}. Ignoring", v);
95            Default::default()
96          }
97        },
98        None => Default::default()
99      };
100
101      let provider_states = ProviderState::from_json(json);
102      let request = json.get("request")
103        .ok_or_else(|| anyhow!("JSON for SynchronousMessages does not contain a 'request' object"))?;
104      let response = json.get("response")
105        .ok_or_else(|| anyhow!("JSON for SynchronousMessages does not contain a 'response' array"))?
106        .as_array()
107        .ok_or_else(|| anyhow!("JSON for SynchronousMessages does not contain a 'response' array"))?;
108      let responses =
109        response.iter()
110          .map(|message| MessageContents::from_json(message))
111          .collect::<Vec<anyhow::Result<MessageContents>>>();
112
113      let plugin_config = parse_plugin_config(json);
114      let interaction_markup = json.get("interactionMarkup")
115        .map(|markup| InteractionMarkup::from_json(markup)).unwrap_or_default();
116
117      let transport = json.get("transport").map(|value| {
118        match value {
119          Value::String(s) => s.clone(),
120          _ => value.to_string()
121        }
122      });
123
124      if responses.iter().any(|res| res.is_err()) {
125        let errors = responses.iter()
126          .filter(|res| res.is_err())
127          .map(|res| res.as_ref().unwrap_err().to_string())
128          .join(", ");
129        Err(anyhow!("Failed to parse SynchronousMessages responses - {}", errors))
130      } else {
131        Ok(SynchronousMessage {
132          id,
133          key,
134          description,
135          provider_states,
136          comments,
137          request: MessageContents::from_json(request)?,
138          response: responses.iter().map(|res| res.as_ref().unwrap().clone()).collect(),
139          pending: json.get("pending")
140            .map(|value| value.as_bool().unwrap_or_default()).unwrap_or_default(),
141          plugin_config,
142          interaction_markup,
143          transport
144        })
145      }
146    } else {
147      Err(anyhow!("Expected a JSON object for the interaction, got '{}'", json))
148    }
149  }
150}
151
152impl V4Interaction for SynchronousMessage {
153  fn to_json(&self) -> Value {
154    let mut json = json!({
155      "type": V4InteractionType::Synchronous_Messages.to_string(),
156      "description": self.description.clone(),
157      "pending": self.pending,
158      "request": self.request.to_json(),
159      "response": self.response.iter().map(|m| m.to_json()).collect_vec()
160    });
161    let map = json.as_object_mut().unwrap();
162
163    if let Some(key) = &self.key {
164      map.insert("key".to_string(), Value::String(key.clone()));
165    }
166
167    if !self.provider_states.is_empty() {
168      map.insert("providerStates".to_string(), Value::Array(
169        self.provider_states.iter().map(|p| p.to_json()).collect()));
170    }
171
172    let comments: Map<String, Value> = self.comments.iter()
173      .filter(|(_k, v)| !is_empty(v))
174      .map(|(k, v)| (k.clone(), v.clone()))
175      .collect();
176    if !comments.is_empty() {
177      map.insert("comments".to_string(), Value::Object(comments));
178    }
179
180    if !self.plugin_config.is_empty() {
181      map.insert("pluginConfiguration".to_string(), self.plugin_config.iter()
182        .map(|(k, v)|
183          (k.clone(), Value::Object(v.iter().map(|(k, v)| (k.clone(), v.clone())).collect()))
184        ).collect());
185    }
186
187    if !self.interaction_markup.is_empty() {
188      map.insert("interactionMarkup".to_string(), self.interaction_markup.to_json());
189    }
190
191    if let Some(transport) = &self.transport {
192      map.insert("transport".to_string(), Value::String(transport.clone()));
193    }
194
195    json
196  }
197
198  fn to_super(&self) -> &(dyn Interaction + Send + Sync + RefUnwindSafe) {
199    self
200  }
201
202  fn to_super_mut(&mut self) -> &mut (dyn Interaction + Send + Sync) {
203    self
204  }
205
206  fn key(&self) -> Option<String> {
207    self.key.clone()
208  }
209
210  fn boxed_v4(&self) -> Box<dyn V4Interaction + Send + Sync + RefUnwindSafe> {
211    Box::new(self.clone())
212  }
213
214  fn comments(&self) -> HashMap<String, Value> {
215    self.comments.clone()
216  }
217
218  fn comments_mut(&mut self) -> &mut HashMap<String, Value> {
219    &mut self.comments
220  }
221
222  fn v4_type(&self) -> V4InteractionType {
223    V4InteractionType::Synchronous_Messages
224  }
225
226  fn plugin_config(&self) -> HashMap<String, HashMap<String, Value>> {
227    self.plugin_config.clone()
228  }
229
230  fn plugin_config_mut(&mut self) -> &mut HashMap<String, HashMap<String, Value>> {
231    &mut self.plugin_config
232  }
233
234  fn interaction_markup(&self) -> InteractionMarkup {
235    self.interaction_markup.clone()
236  }
237
238  fn interaction_markup_mut(&mut self) -> &mut InteractionMarkup {
239    &mut self.interaction_markup
240  }
241
242  fn transport(&self) -> Option<String> {
243    self.transport.clone()
244  }
245
246  fn set_transport(&mut self, transport: Option<String>) {
247    self.transport = transport.clone();
248  }
249
250  fn with_unique_key(&self) -> Box<dyn V4Interaction + Send + Sync + RefUnwindSafe> {
251    Box::new(self.with_key())
252  }
253
254  fn unique_key(&self) -> String {
255    match &self.key {
256      None => self.calc_hash(),
257      Some(key) => key.clone()
258    }
259  }
260}
261
262impl Interaction for SynchronousMessage {
263  fn type_of(&self) -> String {
264    format!("V4 {}", self.v4_type())
265  }
266
267  fn is_request_response(&self) -> bool {
268    false
269  }
270
271  fn as_request_response(&self) -> Option<RequestResponseInteraction> {
272    None
273  }
274
275  fn is_message(&self) -> bool {
276    false
277  }
278
279  fn as_message(&self) -> Option<Message> {
280    None
281  }
282
283  fn id(&self) -> Option<String> {
284    self.id.clone()
285  }
286
287  fn description(&self) -> String {
288    self.description.clone()
289  }
290
291  fn set_id(&mut self, id: Option<String>) {
292    self.id = id;
293  }
294
295  fn set_description(&mut self, description: &str) {
296    self.description = description.to_string();
297  }
298
299  fn provider_states(&self) -> Vec<ProviderState> {
300    self.provider_states.clone()
301  }
302
303  fn provider_states_mut(&mut self) -> &mut Vec<ProviderState> {
304    &mut self.provider_states
305  }
306
307  fn contents(&self) -> OptionalBody {
308    OptionalBody::Missing
309  }
310
311  fn contents_for_verification(&self) -> OptionalBody {
312    self.response.first().map(|message| message.contents.clone()).unwrap_or_default()
313  }
314
315  fn content_type(&self) -> Option<ContentType> {
316    self.request.message_content_type()
317  }
318
319  fn is_v4(&self) -> bool {
320    true
321  }
322
323  fn as_v4(&self) -> Option<Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>> {
324    Some(self.boxed_v4())
325  }
326
327  fn as_v4_mut(&mut self) -> Option<&mut dyn V4Interaction> {
328    Some(self)
329  }
330
331  fn as_v4_http(&self) -> Option<SynchronousHttp> {
332    None
333  }
334
335  fn as_v4_async_message(&self) -> Option<AsynchronousMessage> {
336    None
337  }
338
339  fn as_v4_sync_message(&self) -> Option<SynchronousMessage> {
340    Some(self.clone())
341  }
342
343  fn as_v4_http_mut(&mut self) -> Option<&mut SynchronousHttp> {
344    None
345  }
346
347  fn is_v4_sync_message(&self) -> bool {
348    true
349  }
350
351  fn as_v4_async_message_mut(&mut self) -> Option<&mut AsynchronousMessage> {
352    None
353  }
354
355  fn as_v4_sync_message_mut(&mut self) -> Option<&mut SynchronousMessage> {
356    Some(self)
357  }
358
359  fn boxed(&self) -> Box<dyn Interaction + Send + Sync + RefUnwindSafe> {
360    Box::new(self.clone())
361  }
362
363  fn arced(&self) -> Arc<dyn Interaction + Send + Sync + RefUnwindSafe> {
364    Arc::new(self.clone())
365  }
366
367  fn thread_safe(&self) -> Arc<Mutex<dyn Interaction + Send + Sync + RefUnwindSafe>> {
368    Arc::new(Mutex::new(self.clone()))
369  }
370
371  fn matching_rules(&self) -> Option<MatchingRules> {
372    None
373  }
374
375  fn pending(&self) -> bool {
376    self.pending
377  }
378}
379
380impl Default for SynchronousMessage {
381  fn default() -> Self {
382    SynchronousMessage {
383      id: None,
384      key: None,
385      description: "Synchronous/Message Interaction".to_string(),
386      provider_states: vec![],
387      comments: Default::default(),
388      request: Default::default(),
389      response: Default::default(),
390      pending: false,
391      plugin_config: Default::default(),
392      interaction_markup: Default::default(),
393      transport: None
394    }
395  }
396}
397
398impl PartialEq for SynchronousMessage {
399  fn eq(&self, other: &Self) -> bool {
400    self.key == other.key &&
401      self.description == other.description &&
402      self.provider_states == other.provider_states &&
403      self.request == other.request &&
404      self.response == other.response &&
405      self.pending == other.pending
406  }
407}
408
409impl Hash for SynchronousMessage {
410  fn hash<H: Hasher>(&self, state: &mut H) {
411    self.description.hash(state);
412    self.provider_states.hash(state);
413    self.request.hash(state);
414    self.response.hash(state);
415    self.pending.hash(state);
416  }
417}
418
419impl Display for SynchronousMessage {
420  fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
421    let pending = if self.pending { " [PENDING]" } else { "" };
422    write!(f, "V4 Synchronous Message Interaction{} ( id: {:?}, description: \"{}\", provider_states: {:?}, request: {}, response: {:?} )",
423           pending, self.id, self.description, self.provider_states, self.request, self.response)
424  }
425}
426
427#[cfg(test)]
428mod tests {
429  use expectest::prelude::*;
430  use maplit::hashmap;
431  use pretty_assertions::{assert_eq, assert_ne};
432  use serde_json::{json, Value};
433
434  use crate::bodies::OptionalBody;
435  use crate::prelude::ProviderState;
436  use crate::v4::interaction::V4Interaction;
437  use crate::v4::message_parts::MessageContents;
438  use crate::v4::sync_message::SynchronousMessage;
439
440  #[test]
441  fn calculate_hash_test() {
442    let interaction = SynchronousMessage::from_json(&json!({
443      "description": "a retrieve Mallory request",
444      "pending": false,
445      "providerStates": [
446        {
447          "name": "there is some good mallory"
448        }
449      ],
450      "request": {
451        "contents": {
452          "content": "Mallory",
453          "contentType": "text/plain",
454          "encoded": false
455        },
456        "metadata": {
457          "Content-Type": [
458            "application/json"
459          ]
460        }
461      },
462      "response": [
463        {
464          "contents": {
465            "content": "That is some good Mallory.",
466            "contentType": "text/plain",
467            "encoded": false
468          },
469          "metadata": {
470            "Content-Type": [
471              "text/plain"
472            ]
473          }
474        }
475      ],
476      "type": "Synchronous/Messages"
477    }), 0).unwrap();
478    let hash = interaction.calc_hash();
479    expect!(interaction.calc_hash()).to(be_equal_to(hash.as_str()));
480
481    let interaction2 = interaction.with_key();
482    expect!(interaction2.key.as_ref().unwrap()).to(be_equal_to(hash.as_str()));
483
484    let json = interaction2.to_json();
485    assert_eq!(json!({
486      "description": "a retrieve Mallory request",
487      "key": "93f58446f133592f",
488      "pending": false,
489      "providerStates": [
490        {
491          "name": "there is some good mallory"
492        }
493      ],
494      "request": {
495        "contents": {
496            "content": "Mallory",
497            "contentType": "text/plain",
498            "encoded": false
499         },
500        "metadata": {
501             "Content-Type": [
502                "application/json"
503             ]
504        }
505      },
506      "response": [{
507        "contents": {
508          "content": "That is some good Mallory.",
509          "contentType": "text/plain",
510          "encoded": false
511        },
512        "metadata": {
513          "Content-Type": [
514            "text/plain"
515          ]
516        }
517      }],
518      "type": "Synchronous/Messages"
519    }), json);
520  }
521
522  #[test]
523  fn hash_test() {
524    let i1 = SynchronousMessage::default();
525    expect!(i1.calc_hash()).to(be_equal_to("2c18fa761d06be45"));
526
527    let i2 = SynchronousMessage {
528      description: "a retrieve Mallory request".to_string(),
529      .. SynchronousMessage::default()
530    };
531    expect!(i2.calc_hash()).to(be_equal_to("66fbdb308329891b"));
532
533    let i3 = SynchronousMessage {
534      description: "a retrieve Mallory request".to_string(),
535      provider_states: vec![ProviderState::default("there is some good mallory")],
536      .. SynchronousMessage::default()
537    };
538    expect!(i3.calc_hash()).to(be_equal_to("831a3fa6d0a7ea0c"));
539
540    let i4 = SynchronousMessage {
541      description: "a retrieve Mallory request".to_string(),
542      provider_states: vec![ProviderState::default("there is some good mallory")],
543      request: MessageContents {
544        contents: OptionalBody::from("That is some good Mallory."),
545        .. MessageContents::default()
546      },
547      .. SynchronousMessage::default()
548    };
549    expect!(i4.calc_hash()).to(be_equal_to("25420754ce64549d"));
550
551    let i5 = SynchronousMessage {
552      description: "a retrieve Mallory request".to_string(),
553      provider_states: vec![ProviderState::default("there is some good mallory")],
554      request: MessageContents {
555        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string())  },
556        contents: OptionalBody::from("That is some good Mallory."),
557        .. MessageContents::default()
558      },
559      .. SynchronousMessage::default()
560    };
561    expect!(i5.calc_hash()).to(be_equal_to("aefc777fdfa238b0"));
562
563    let i6 = SynchronousMessage {
564      description: "a retrieve Mallory request".to_string(),
565      provider_states: vec![ProviderState::default("there is some good mallory")],
566      request: MessageContents {
567        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string()) },
568        contents: OptionalBody::from("That is some good Mallory."),
569        .. MessageContents::default()
570      },
571      response: vec![MessageContents {
572        metadata: hashmap!{ "Content-Type".to_string() => Value::String("text/plain".to_string()) },
573        contents: OptionalBody::from("That is some good Mallory."),
574        .. MessageContents::default()
575      }],
576      .. SynchronousMessage::default()
577    };
578    expect!(i6.calc_hash()).to(be_equal_to("9338c66e694d3d80"));
579  }
580
581  #[test]
582  fn equals_test() {
583    let i1 = SynchronousMessage::default();
584    let i2 = SynchronousMessage {
585      description: "a retrieve Mallory request".to_string(),
586      .. SynchronousMessage::default()
587    };
588    let i3 = SynchronousMessage {
589      description: "a retrieve Mallory request".to_string(),
590      provider_states: vec![ProviderState::default("there is some good mallory")],
591      .. SynchronousMessage::default()
592    };
593    let i4 = SynchronousMessage {
594      description: "a retrieve Mallory request".to_string(),
595      provider_states: vec![ProviderState::default("there is some good mallory")],
596      request: MessageContents {
597        contents: OptionalBody::from("That is some good Mallory."),
598        .. MessageContents::default()
599      },
600      .. SynchronousMessage::default()
601    };
602    let i5 = SynchronousMessage {
603      description: "a retrieve Mallory request".to_string(),
604      provider_states: vec![ProviderState::default("there is some good mallory")],
605      request: MessageContents {
606        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string())  },
607        contents: OptionalBody::from("That is some good Mallory."),
608        .. MessageContents::default()
609      },
610      .. SynchronousMessage::default()
611    };
612    let i6 = SynchronousMessage {
613      description: "a retrieve Mallory request".to_string(),
614      provider_states: vec![ProviderState::default("there is some good mallory")],
615      request: MessageContents {
616        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string()) },
617        contents: OptionalBody::from("That is some good Mallory."),
618        .. MessageContents::default()
619      },
620      response: vec![MessageContents {
621        metadata: hashmap!{ "Content-Type".to_string() => Value::String("text/plain".to_string()) },
622        contents: OptionalBody::from("That is some good Mallory."),
623        .. MessageContents::default()
624      }],
625      .. SynchronousMessage::default()
626    };
627
628    assert_eq!(i1, i1);
629    assert_eq!(i2, i2);
630    assert_eq!(i3, i3);
631    assert_eq!(i4, i4);
632    assert_eq!(i5, i5);
633    assert_eq!(i6, i6);
634
635    assert_ne!(i1, i2);
636    assert_ne!(i1, i3);
637    assert_ne!(i1, i4);
638    assert_ne!(i1, i5);
639    assert_ne!(i1, i6);
640    assert_ne!(i2, i1);
641    assert_ne!(i2, i3);
642    assert_ne!(i2, i4);
643    assert_ne!(i2, i5);
644    assert_ne!(i2, i6);
645  }
646
647  #[test]
648  fn equals_test_with_different_keys() {
649    let i1 = SynchronousMessage {
650      key: Some("i1".to_string()),
651      description: "a retrieve Mallory request".to_string(),
652      provider_states: vec![ProviderState::default("there is some good mallory")],
653      request: MessageContents {
654        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string()) },
655        contents: OptionalBody::from("That is some good Mallory."),
656        .. MessageContents::default()
657      },
658      response: vec![MessageContents {
659        metadata: hashmap!{ "Content-Type".to_string() => Value::String("text/plain".to_string()) },
660        contents: OptionalBody::from("That is some good Mallory."),
661        .. MessageContents::default()
662      }],
663      .. SynchronousMessage::default()
664    };
665    let i2 = SynchronousMessage {
666      key: Some("i2".to_string()),
667      description: "a retrieve Mallory request".to_string(),
668      provider_states: vec![ProviderState::default("there is some good mallory")],
669      request: MessageContents {
670        metadata: hashmap!{ "Content-Type".to_string() => Value::String("application/json".to_string()) },
671        contents: OptionalBody::from("That is some good Mallory."),
672        .. MessageContents::default()
673      },
674      response: vec![MessageContents {
675        metadata: hashmap!{ "Content-Type".to_string() => Value::String("text/plain".to_string()) },
676        contents: OptionalBody::from("That is some good Mallory."),
677        .. MessageContents::default()
678      }],
679      .. SynchronousMessage::default()
680    };
681
682    assert_eq!(i1, i1);
683    assert_eq!(i2, i2);
684
685    assert_ne!(i1, i2);
686    assert_ne!(i2, i1);
687  }
688}