1use 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
51pub 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
76pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
78
79#[repr(C)]
81#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Deserialize, Serialize)]
82#[allow(non_camel_case_types)]
83pub enum PactSpecification {
84 Unknown,
86 V1,
88 V1_1,
90 V2,
92 V3,
94 V4
96}
97
98impl Default for PactSpecification {
99 fn default() -> Self {
100 PactSpecification::Unknown
101 }
102}
103
104impl PactSpecification {
105 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 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#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
185pub struct Consumer {
186 pub name: String
188}
189
190impl Consumer {
191 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 pub fn to_json(&self) -> Value {
205 json!({ "name" : self.name })
206 }
207
208 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#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
255pub struct Provider {
256 pub name: String
258}
259
260impl Provider {
261 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 pub fn to_json(&self) -> Value {
275 json!({ "name" : self.name })
276 }
277
278 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#[derive(PartialEq, Debug, Clone, Eq)]
326pub enum DifferenceType {
327 Method,
329 Path,
331 Headers,
333 QueryParameters,
335 Body,
337 MatchingRules,
339 Status
341}
342
343#[derive(Debug, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)]
345pub enum HttpStatus {
346 Information,
348 Success,
350 Redirect,
352 ClientError,
354 ServerError,
356 StatusCodes(Vec<u16>),
358 NonError,
360 Error
362}
363
364impl HttpStatus {
365 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 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}