pact_models/
lib.rs

1//! The `pact_models` crate provides all the structs and traits required to model a Pact.
2//!
3//! ## Crate features
4//!
5//! All features are enabled by default
6//!
7//! * `datetime`: Enables support of date and time expressions and generators. This will add the
8//! `chronos` crate as a dependency.
9//! * `xml`: Enables support for parsing XML documents. This feature will add the `sxd-document`
10//! crate as a dependency.
11
12use std::fmt::{Display, Formatter};
13use std::fmt;
14
15use anyhow::anyhow;
16use itertools::Itertools;
17use serde::{Deserialize, Serialize};
18use serde_json::{json, Value};
19
20use crate::verify_json::{json_type_of, PactFileVerificationResult, PactJsonVerifier, ResultLevel};
21
22pub mod content_types;
23pub mod bodies;
24pub mod v4;
25pub mod provider_states;
26pub mod verify_json;
27pub mod json_utils;
28pub mod expression_parser;
29#[cfg(feature = "datetime")] pub mod time_utils;
30#[cfg(feature = "datetime")] mod timezone_db;
31#[cfg(not(target_family = "wasm"))] pub mod file_utils;
32#[cfg(feature = "xml")] pub mod xml_utils;
33pub mod matchingrules;
34pub mod generators;
35pub mod path_exp;
36pub mod query_strings;
37#[cfg(not(target_family = "wasm"))] pub mod http_utils;
38pub mod http_parts;
39pub mod request;
40pub mod response;
41pub mod headers;
42pub mod interaction;
43pub mod sync_interaction;
44pub mod message;
45pub mod pact;
46pub mod sync_pact;
47pub mod message_pact;
48mod iterator_utils;
49pub mod plugins;
50
51/// A "prelude" or a default list of import types to include.
52pub mod prelude {
53  pub use crate::{Consumer, Provider};
54  pub use crate::bodies::OptionalBody;
55  pub use crate::content_types::ContentType;
56  pub use crate::expression_parser::DataType;
57  pub use crate::generators::{Generator, GeneratorCategory, Generators};
58  pub use crate::interaction::Interaction;
59  pub use crate::matchingrules::{Category, MatchingRuleCategory, MatchingRules, RuleLogic};
60  pub use crate::message_pact::MessagePact;
61  pub use crate::pact::Pact;
62  pub use crate::PactSpecification;
63  pub use crate::provider_states::ProviderState;
64  pub use crate::request::Request;
65  pub use crate::response::Response;
66  pub use crate::sync_interaction::RequestResponseInteraction;
67  pub use crate::sync_pact::RequestResponsePact;
68  #[cfg(not(target_family = "wasm"))] pub use crate::http_utils::HttpAuth;
69
70  pub mod v4 {
71    pub use crate::v4::pact::V4Pact;
72    pub use crate::v4::synch_http::SynchronousHttp;
73  }
74}
75
76/// Version of the library
77pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
78
79/// Enum defining the pact specification versions supported by the library
80#[repr(C)]
81#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Deserialize, Serialize)]
82#[allow(non_camel_case_types)]
83pub enum PactSpecification {
84  /// Unknown or unsupported specification version
85  Unknown,
86  /// First version of the pact specification (`https://github.com/pact-foundation/pact-specification/tree/version-1`)
87  V1,
88  /// Second version of the pact specification (`https://github.com/pact-foundation/pact-specification/tree/version-1.1`)
89  V1_1,
90  /// Version two of the pact specification (`https://github.com/pact-foundation/pact-specification/tree/version-2`)
91  V2,
92  /// Version three of the pact specification (`https://github.com/pact-foundation/pact-specification/tree/version-3`)
93  V3,
94  /// Version four of the pact specification (`https://github.com/pact-foundation/pact-specification/tree/version-4`)
95  V4
96}
97
98impl Default for PactSpecification {
99  fn default() -> Self {
100        PactSpecification::Unknown
101    }
102}
103
104impl PactSpecification {
105  /// Returns the semantic version string of the specification version.
106  pub fn version_str(&self) -> String {
107    match *self {
108        PactSpecification::V1 => "1.0.0",
109        PactSpecification::V1_1 => "1.1.0",
110        PactSpecification::V2 => "2.0.0",
111        PactSpecification::V3 => "3.0.0",
112        PactSpecification::V4 => "4.0",
113        _ => "unknown"
114    }.into()
115  }
116
117  /// Parse a version string into a PactSpecification
118  pub fn parse_version<S: Into<String>>(input: S) -> anyhow::Result<PactSpecification> {
119    let str_version = input.into();
120    let version = lenient_semver::parse(str_version.as_str())
121      .map_err(|_| anyhow!("Invalid specification version '{}'", str_version))?;
122    match version.major {
123      1 => match version.minor {
124        0 => Ok(PactSpecification::V1),
125        1 => Ok(PactSpecification::V1_1),
126        _ => Err(anyhow!("Unsupported specification version '{}'", str_version))
127      },
128      2 => match version.minor {
129        0 => Ok(PactSpecification::V2),
130        _ => Err(anyhow!("Unsupported specification version '{}'", str_version))
131      },
132      3 => match version.minor {
133        0 => Ok(PactSpecification::V3),
134        _ => Err(anyhow!("Unsupported specification version '{}'", str_version))
135      },
136      4 => match version.minor {
137        0 => Ok(PactSpecification::V4),
138        _ => Err(anyhow!("Unsupported specification version '{}'", str_version))
139      },
140      _ => Err(anyhow!("Invalid specification version '{}'", str_version))
141    }
142  }
143}
144
145impl From<&str> for PactSpecification {
146  fn from(s: &str) -> Self {
147    match s.to_uppercase().as_str() {
148      "V1" => PactSpecification::V1,
149      "V1.1" => PactSpecification::V1_1,
150      "V2" => PactSpecification::V2,
151      "V3" => PactSpecification::V3,
152      "V4" => PactSpecification::V4,
153      _ => PactSpecification::Unknown
154    }
155  }
156}
157
158impl From<String> for PactSpecification {
159  fn from(s: String) -> Self {
160    PactSpecification::from(s.as_str())
161  }
162}
163
164impl From<&String> for PactSpecification {
165  fn from(s: &String) -> Self {
166    PactSpecification::from(s.as_str())
167  }
168}
169
170impl Display for PactSpecification {
171  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172    match *self {
173      PactSpecification::V1 => write!(f, "V1"),
174      PactSpecification::V1_1 => write!(f, "V1.1"),
175      PactSpecification::V2 => write!(f, "V2"),
176      PactSpecification::V3 => write!(f, "V3"),
177      PactSpecification::V4 => write!(f, "V4"),
178      _ => write!(f, "unknown")
179    }
180  }
181}
182
183/// Struct that defines the consumer of the pact.
184#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
185pub struct Consumer {
186  /// Each consumer should have a unique name to identify it.
187  pub name: String
188}
189
190impl Consumer {
191  /// Builds a `Consumer` from the `Json` struct.
192  pub fn from_json(pact_json: &Value) -> Consumer {
193    let val = match pact_json.get("name") {
194      Some(v) => match v.clone() {
195        Value::String(s) => s,
196        _ => v.to_string()
197      },
198      None => "consumer".to_string()
199    };
200    Consumer { name: val }
201  }
202
203  /// Converts this `Consumer` to a `Value` struct.
204  pub fn to_json(&self) -> Value {
205    json!({ "name" : self.name })
206  }
207
208  /// Generate the JSON schema properties for the given Pact specification
209  pub fn schema(_spec_version: PactSpecification) -> Value {
210    json!({
211      "properties": {
212        "name": {
213          "type": "string"
214        }
215      },
216      "required": ["name"],
217      "type": "object"
218    })
219  }
220}
221
222impl PactJsonVerifier for Consumer {
223  fn verify_json(path: &str, pact_json: &Value, strict: bool, _spec_version: PactSpecification) -> Vec<PactFileVerificationResult> {
224    let mut results = vec![];
225
226    match pact_json {
227      Value::Object(values) => {
228        if let Some(name) = values.get("name") {
229          if !name.is_string() {
230            results.push(PactFileVerificationResult::new(path.to_owned() + "/name", ResultLevel::ERROR,
231              format!("Must be a String, got {}", json_type_of(pact_json))))
232          }
233        } else {
234          results.push(PactFileVerificationResult::new(path.to_owned() + "/name",
235            if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, "Missing name"))
236        }
237
238        for key in values.keys() {
239          if key != "name" {
240            results.push(PactFileVerificationResult::new(path.to_owned(),
241              if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, format!("Unknown attribute '{}'", key)))
242          }
243        }
244      }
245      _ => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
246        format!("Must be an Object, got {}", json_type_of(pact_json))))
247    }
248
249    results
250  }
251}
252
253/// Struct that defines a provider of a pact.
254#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
255pub struct Provider {
256  /// Each provider should have a unique name to identify it.
257  pub name: String
258}
259
260impl Provider {
261  /// Builds a `Provider` from a `Value` struct.
262  pub fn from_json(pact_json: &Value) -> Provider {
263    let val = match pact_json.get("name") {
264      Some(v) => match v.clone() {
265        Value::String(s) => s,
266        _ => v.to_string()
267      },
268      None => "provider".to_string()
269    };
270    Provider { name: val }
271  }
272
273  /// Converts this `Provider` to a `Value` struct.
274  pub fn to_json(&self) -> Value {
275    json!({ "name" : self.name })
276  }
277
278  /// Generate the JSON schema properties for the given Pact specification
279  pub fn schema(_spec_version: PactSpecification) -> Value {
280    json!({
281      "properties": {
282        "name": {
283          "type": "string"
284        }
285      },
286      "required": ["name"],
287      "type": "object"
288    })
289  }
290}
291
292impl PactJsonVerifier for Provider {
293  fn verify_json(path: &str, pact_json: &Value, strict: bool, _spec_version: PactSpecification) -> Vec<PactFileVerificationResult> {
294    let mut results = vec![];
295
296    match pact_json {
297      Value::Object(values) => {
298        if let Some(name) = values.get("name") {
299          if !name.is_string() {
300            results.push(PactFileVerificationResult::new(path.to_owned() + "/name", ResultLevel::ERROR,
301                                                         format!("Must be a String, got {}", json_type_of(pact_json))))
302          }
303        } else {
304          results.push(PactFileVerificationResult::new(path.to_owned() + "/name",
305            if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, "Missing name"))
306        }
307
308        for key in values.keys() {
309          if key != "name" {
310            results.push(PactFileVerificationResult::new(path.to_owned(),
311              if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, format!("Unknown attribute '{}'", key)))
312          }
313        }
314      }
315      _ => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
316                                                        format!("Must be an Object, got {}", json_type_of(pact_json))))
317    }
318
319    results
320  }
321}
322
323
324/// Enumeration of the types of differences between requests and responses
325#[derive(PartialEq, Debug, Clone, Eq)]
326pub enum DifferenceType {
327  /// Methods differ
328  Method,
329  /// Paths differ
330  Path,
331  /// Headers differ
332  Headers,
333  /// Query parameters differ
334  QueryParameters,
335  /// Bodies differ
336  Body,
337  /// Matching Rules differ
338  MatchingRules,
339  /// Response status differ
340  Status
341}
342
343/// Enum that defines the different types of HTTP statuses
344#[derive(Debug, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)]
345pub enum HttpStatus {
346  /// Informational responses (100–199)
347  Information,
348  /// Successful responses (200–299)
349  Success,
350  /// Redirects (300–399)
351  Redirect,
352  /// Client errors (400–499)
353  ClientError,
354  /// Server errors (500–599)
355  ServerError,
356  /// Explicit status codes
357  StatusCodes(Vec<u16>),
358  /// Non-error response(< 400)
359  NonError,
360  /// Any error response (>= 400)
361  Error
362}
363
364impl HttpStatus {
365  /// Parse a JSON structure into a HttpStatus
366  pub fn from_json(value: &Value) -> anyhow::Result<Self> {
367    match value {
368      Value::String(s) => match s.as_str() {
369        "info" => Ok(HttpStatus::Information),
370        "success" => Ok(HttpStatus::Success),
371        "redirect" => Ok(HttpStatus::Redirect),
372        "clientError" => Ok(HttpStatus::ClientError),
373        "serverError" => Ok(HttpStatus::ServerError),
374        "nonError" => Ok(HttpStatus::NonError),
375        "error" => Ok(HttpStatus::Error),
376        _ => Err(anyhow!("'{}' is not a valid value for an HTTP Status", s))
377      },
378      Value::Array(a) => {
379        let status_codes = a.iter().map(|status| match status {
380          Value::Number(n) => if n.is_u64() {
381            Ok(n.as_u64().unwrap() as u16)
382          } else if n.is_i64() {
383            Ok(n.as_i64().unwrap() as u16)
384          } else {
385            Ok(n.as_f64().unwrap() as u16)
386          },
387          Value::String(s) => s.parse::<u16>().map_err(|err| anyhow!(err)),
388          _ => Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", status))
389        }).collect::<Vec<anyhow::Result<u16>>>();
390        if status_codes.iter().any(|it| it.is_err()) {
391          Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", value))
392        } else {
393          Ok(HttpStatus::StatusCodes(status_codes.iter().map(|code| *code.as_ref().unwrap()).collect()))
394        }
395      }
396      _ => Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", value))
397    }
398  }
399
400  /// Generate a JSON structure for this status
401  pub fn to_json(&self) -> Value {
402    match self {
403      HttpStatus::StatusCodes(codes) => json!(codes),
404      HttpStatus::Information => json!("info"),
405      HttpStatus::Success => json!("success"),
406      HttpStatus::Redirect => json!("redirect"),
407      HttpStatus::ClientError => json!("clientError"),
408      HttpStatus::ServerError => json!("serverError"),
409      HttpStatus::NonError => json!("nonError"),
410      HttpStatus::Error => json!("error")
411    }
412  }
413}
414
415impl Display for HttpStatus {
416  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
417    match self {
418      HttpStatus::Information => write!(f, "Informational response (100–199)"),
419      HttpStatus::Success => write!(f, "Successful response (200–299)"),
420      HttpStatus::Redirect => write!(f, "Redirect (300–399)"),
421      HttpStatus::ClientError => write!(f, "Client error (400–499)"),
422      HttpStatus::ServerError => write!(f, "Server error (500–599)"),
423      HttpStatus::StatusCodes(status) =>
424        write!(f, "{}", status.iter().map(|s| s.to_string()).join(", ")),
425      HttpStatus::NonError => write!(f, "Non-error response (< 400)"),
426      HttpStatus::Error => write!(f, "Error response (>= 400)")
427    }
428  }
429}
430
431#[cfg(test)]
432mod tests;
433
434#[cfg(test)]
435pub struct Contains {
436  expected: String
437}
438
439#[cfg(test)]
440pub fn contain<S: Into<String>>(expected: S) -> Contains {
441  Contains { expected: expected.into() }
442}
443
444#[cfg(test)]
445impl<A> expectest::core::Matcher<A, String> for Contains
446  where
447    A: Into<String> + Clone
448{
449  fn failure_message(&self, _join: expectest::core::Join, actual: &A) -> String {
450    let s: String = actual.clone().into();
451    format!("expected '{}' to contain <{:?}>", s, self.expected)
452  }
453
454  fn matches(&self, actual: &A) -> bool {
455    let s: String = actual.clone().into();
456    s.contains(self.expected.as_str())
457  }
458}