use std::fmt::{Display, Formatter};
use std::fmt;
use anyhow::anyhow;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::verify_json::{json_type_of, PactFileVerificationResult, PactJsonVerifier, ResultLevel};
pub mod content_types;
pub mod bodies;
pub mod v4;
pub mod provider_states;
pub mod verify_json;
pub mod json_utils;
pub mod expression_parser;
pub mod time_utils;
mod timezone_db;
#[cfg(not(target_family = "wasm"))] pub mod file_utils;
pub mod xml_utils;
pub mod matchingrules;
pub mod generators;
pub mod path_exp;
pub mod query_strings;
#[cfg(not(target_family = "wasm"))] pub mod http_utils;
pub mod http_parts;
pub mod request;
pub mod response;
pub mod headers;
pub mod interaction;
pub mod sync_interaction;
pub mod message;
pub mod pact;
pub mod sync_pact;
pub mod message_pact;
mod iterator_utils;
pub mod plugins;
pub mod prelude {
pub use crate::{Consumer, Provider};
pub use crate::bodies::OptionalBody;
pub use crate::content_types::ContentType;
pub use crate::expression_parser::DataType;
pub use crate::generators::{Generator, GeneratorCategory, Generators};
pub use crate::interaction::Interaction;
pub use crate::matchingrules::{Category, MatchingRuleCategory, MatchingRules, RuleLogic};
pub use crate::message_pact::MessagePact;
pub use crate::pact::Pact;
pub use crate::PactSpecification;
pub use crate::provider_states::ProviderState;
pub use crate::request::Request;
pub use crate::response::Response;
pub use crate::sync_interaction::RequestResponseInteraction;
pub use crate::sync_pact::RequestResponsePact;
#[cfg(not(target_family = "wasm"))] pub use crate::http_utils::HttpAuth;
pub mod v4 {
pub use crate::v4::pact::V4Pact;
pub use crate::v4::synch_http::SynchronousHttp;
}
}
pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Deserialize, Serialize)]
#[allow(non_camel_case_types)]
pub enum PactSpecification {
Unknown,
V1,
V1_1,
V2,
V3,
V4
}
impl Default for PactSpecification {
fn default() -> Self {
PactSpecification::Unknown
}
}
impl PactSpecification {
pub fn version_str(&self) -> String {
match *self {
PactSpecification::V1 => "1.0.0",
PactSpecification::V1_1 => "1.1.0",
PactSpecification::V2 => "2.0.0",
PactSpecification::V3 => "3.0.0",
PactSpecification::V4 => "4.0",
_ => "unknown"
}.into()
}
pub fn parse_version<S: Into<String>>(input: S) -> anyhow::Result<PactSpecification> {
let str_version = input.into();
let version = lenient_semver::parse(str_version.as_str())
.map_err(|_| anyhow!("Invalid specification version '{}'", str_version))?;
match version.major {
1 => match version.minor {
0 => Ok(PactSpecification::V1),
1 => Ok(PactSpecification::V1_1),
_ => Err(anyhow!("Unsupported specification version '{}'", str_version))
},
2 => match version.minor {
0 => Ok(PactSpecification::V2),
_ => Err(anyhow!("Unsupported specification version '{}'", str_version))
},
3 => match version.minor {
0 => Ok(PactSpecification::V3),
_ => Err(anyhow!("Unsupported specification version '{}'", str_version))
},
4 => match version.minor {
0 => Ok(PactSpecification::V4),
_ => Err(anyhow!("Unsupported specification version '{}'", str_version))
},
_ => Err(anyhow!("Invalid specification version '{}'", str_version))
}
}
}
impl From<&str> for PactSpecification {
fn from(s: &str) -> Self {
match s.to_uppercase().as_str() {
"V1" => PactSpecification::V1,
"V1.1" => PactSpecification::V1_1,
"V2" => PactSpecification::V2,
"V3" => PactSpecification::V3,
"V4" => PactSpecification::V4,
_ => PactSpecification::Unknown
}
}
}
impl From<String> for PactSpecification {
fn from(s: String) -> Self {
PactSpecification::from(s.as_str())
}
}
impl From<&String> for PactSpecification {
fn from(s: &String) -> Self {
PactSpecification::from(s.as_str())
}
}
impl Display for PactSpecification {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
PactSpecification::V1 => write!(f, "V1"),
PactSpecification::V1_1 => write!(f, "V1.1"),
PactSpecification::V2 => write!(f, "V2"),
PactSpecification::V3 => write!(f, "V3"),
PactSpecification::V4 => write!(f, "V4"),
_ => write!(f, "unknown")
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct Consumer {
pub name: String
}
impl Consumer {
pub fn from_json(pact_json: &Value) -> Consumer {
let val = match pact_json.get("name") {
Some(v) => match v.clone() {
Value::String(s) => s,
_ => v.to_string()
},
None => "consumer".to_string()
};
Consumer { name: val }
}
pub fn to_json(&self) -> Value {
json!({ "name" : self.name })
}
pub fn schema(_spec_version: PactSpecification) -> Value {
json!({
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"],
"type": "object"
})
}
}
impl PactJsonVerifier for Consumer {
fn verify_json(path: &str, pact_json: &Value, strict: bool, _spec_version: PactSpecification) -> Vec<PactFileVerificationResult> {
let mut results = vec![];
match pact_json {
Value::Object(values) => {
if let Some(name) = values.get("name") {
if !name.is_string() {
results.push(PactFileVerificationResult::new(path.to_owned() + "/name", ResultLevel::ERROR,
format!("Must be a String, got {}", json_type_of(pact_json))))
}
} else {
results.push(PactFileVerificationResult::new(path.to_owned() + "/name",
if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, "Missing name"))
}
for key in values.keys() {
if key != "name" {
results.push(PactFileVerificationResult::new(path.to_owned(),
if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, format!("Unknown attribute '{}'", key)))
}
}
}
_ => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
format!("Must be an Object, got {}", json_type_of(pact_json))))
}
results
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct Provider {
pub name: String
}
impl Provider {
pub fn from_json(pact_json: &Value) -> Provider {
let val = match pact_json.get("name") {
Some(v) => match v.clone() {
Value::String(s) => s,
_ => v.to_string()
},
None => "provider".to_string()
};
Provider { name: val }
}
pub fn to_json(&self) -> Value {
json!({ "name" : self.name })
}
pub fn schema(_spec_version: PactSpecification) -> Value {
json!({
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"],
"type": "object"
})
}
}
impl PactJsonVerifier for Provider {
fn verify_json(path: &str, pact_json: &Value, strict: bool, _spec_version: PactSpecification) -> Vec<PactFileVerificationResult> {
let mut results = vec![];
match pact_json {
Value::Object(values) => {
if let Some(name) = values.get("name") {
if !name.is_string() {
results.push(PactFileVerificationResult::new(path.to_owned() + "/name", ResultLevel::ERROR,
format!("Must be a String, got {}", json_type_of(pact_json))))
}
} else {
results.push(PactFileVerificationResult::new(path.to_owned() + "/name",
if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, "Missing name"))
}
for key in values.keys() {
if key != "name" {
results.push(PactFileVerificationResult::new(path.to_owned(),
if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, format!("Unknown attribute '{}'", key)))
}
}
}
_ => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
format!("Must be an Object, got {}", json_type_of(pact_json))))
}
results
}
}
#[derive(PartialEq, Debug, Clone, Eq)]
pub enum DifferenceType {
Method,
Path,
Headers,
QueryParameters,
Body,
MatchingRules,
Status
}
#[derive(Debug, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)]
pub enum HttpStatus {
Information,
Success,
Redirect,
ClientError,
ServerError,
StatusCodes(Vec<u16>),
NonError,
Error
}
impl HttpStatus {
pub fn from_json(value: &Value) -> anyhow::Result<Self> {
match value {
Value::String(s) => match s.as_str() {
"info" => Ok(HttpStatus::Information),
"success" => Ok(HttpStatus::Success),
"redirect" => Ok(HttpStatus::Redirect),
"clientError" => Ok(HttpStatus::ClientError),
"serverError" => Ok(HttpStatus::ServerError),
"nonError" => Ok(HttpStatus::NonError),
"error" => Ok(HttpStatus::Error),
_ => Err(anyhow!("'{}' is not a valid value for an HTTP Status", s))
},
Value::Array(a) => {
let status_codes = a.iter().map(|status| match status {
Value::Number(n) => if n.is_u64() {
Ok(n.as_u64().unwrap() as u16)
} else if n.is_i64() {
Ok(n.as_i64().unwrap() as u16)
} else {
Ok(n.as_f64().unwrap() as u16)
},
Value::String(s) => s.parse::<u16>().map_err(|err| anyhow!(err)),
_ => Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", status))
}).collect::<Vec<anyhow::Result<u16>>>();
if status_codes.iter().any(|it| it.is_err()) {
Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", value))
} else {
Ok(HttpStatus::StatusCodes(status_codes.iter().map(|code| *code.as_ref().unwrap()).collect()))
}
}
_ => Err(anyhow!("'{}' is not a valid JSON value for an HTTP Status", value))
}
}
pub fn to_json(&self) -> Value {
match self {
HttpStatus::StatusCodes(codes) => json!(codes),
HttpStatus::Information => json!("info"),
HttpStatus::Success => json!("success"),
HttpStatus::Redirect => json!("redirect"),
HttpStatus::ClientError => json!("clientError"),
HttpStatus::ServerError => json!("serverError"),
HttpStatus::NonError => json!("nonError"),
HttpStatus::Error => json!("error")
}
}
}
impl Display for HttpStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
HttpStatus::Information => write!(f, "Informational response (100–199)"),
HttpStatus::Success => write!(f, "Successful response (200–299)"),
HttpStatus::Redirect => write!(f, "Redirect (300–399)"),
HttpStatus::ClientError => write!(f, "Client error (400–499)"),
HttpStatus::ServerError => write!(f, "Server error (500–599)"),
HttpStatus::StatusCodes(status) =>
write!(f, "{}", status.iter().map(|s| s.to_string()).join(", ")),
HttpStatus::NonError => write!(f, "Non-error response (< 400)"),
HttpStatus::Error => write!(f, "Error response (>= 400)")
}
}
}
#[cfg(test)]
mod tests;