1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! Executor abstraction for executing callbacks to user code (request filters, provider state change callbacks)

use pact_matching::models::{Request, OptionalBody};
use pact_matching::models::provider_states::ProviderState;
use crate::provider_client::{make_state_change_request, provider_client_error_to_string};
use std::collections::HashMap;
use serde_json::{Value, json};
use ansi_term::Colour::Yellow;
use async_trait::async_trait;
use maplit::*;

/// Trait for executors that call request filters
pub trait RequestFilterExecutor {
  /// Mutates requests based on some criteria.
  fn call(&self, request: &Request) -> Request;
}

/// A "null" request filter executor, which does nothing, but permits
/// bypassing of typechecking issues where no filter should be applied.
pub struct NullRequestFilterExecutor {
  // This field is added (and is private) to guarantee that this struct
  // is never instantiated accidentally, and is instead only able to be
  // used for type-level programming.
  _private_field: (),
}

impl RequestFilterExecutor for NullRequestFilterExecutor {
  fn call(&self, _request: &Request) -> Request {
    unimplemented!("NullRequestFilterExecutor should never be called")
  }
}

/// Struct for returning errors from executing a provider state
#[derive(Debug, Clone)]
pub struct ProviderStateError {
  /// Description of the error
  pub description: String,
  /// Interaction ID of the interaction that the error occurred
  pub interaction_id: Option<String>
}

/// Trait for executors that call provider state callbacks
#[async_trait]
pub trait ProviderStateExecutor {
  /// Invoke the callback for the given provider state, returning an optional Map of values
  async fn call(&self, interaction_id: Option<String>, provider_state: &ProviderState, setup: bool, client: Option<&reqwest::Client>) -> Result<HashMap<String, Value>, ProviderStateError>;
}

/// Default provider state callback executor, which executes an HTTP request
pub struct HttpRequestProviderStateExecutor {
  /// URL to post state change requests to
  pub state_change_url: Option<String>,
  /// If teardown state change requests should be made (default is false)
  pub state_change_teardown: bool,
  /// If state change request data should be sent in the body (true) or as query parameters (false)
  pub state_change_body: bool
}

impl Default for HttpRequestProviderStateExecutor {
  /// Create a default executor
  fn default() -> HttpRequestProviderStateExecutor {
    HttpRequestProviderStateExecutor {
      state_change_url: None,
      state_change_teardown: false,
      state_change_body: true
    }
  }
}

#[async_trait]
impl ProviderStateExecutor for HttpRequestProviderStateExecutor {
  async fn call(&self, interaction_id: Option<String>, provider_state: &ProviderState, setup: bool, client: Option<&reqwest::Client>) -> Result<HashMap<String, Value>, ProviderStateError> {
    match &self.state_change_url {
      Some(state_change_url) => {
        let mut state_change_request = Request { method: "POST".to_string(), .. Request::default() };
        if self.state_change_body {
          let mut json_body = json!({
                    "state".to_string() : json!(provider_state.name.clone()),
                    "action".to_string() : json!(if setup {
                        "setup".to_string()
                    } else {
                        "teardown".to_string()
                    })
                });
          {
            let json_body_mut = json_body.as_object_mut().unwrap();
            for (k, v) in provider_state.params.clone() {
              json_body_mut.insert(k, v);
            }
          }
          state_change_request.body = OptionalBody::Present(json_body.to_string().into());
          state_change_request.headers = Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] });
        } else {
          let mut query = hashmap!{ "state".to_string() => vec![provider_state.name.clone()] };
          if setup {
            query.insert("action".to_string(), vec!["setup".to_string()]);
          } else {
            query.insert("action".to_string(), vec!["teardown".to_string()]);
          }
          for (k, v) in provider_state.params.clone() {
            query.insert(k, vec![match v {
              Value::String(ref s) => s.clone(),
              _ => v.to_string()
            }]);
          }
          state_change_request.query = Some(query);
        }
        match make_state_change_request(client.unwrap_or(&reqwest::Client::default()), &state_change_url, &state_change_request).await {
          Ok(_) => Ok(hashmap!{}),
          Err(err) => Err(ProviderStateError {
            description: provider_client_error_to_string(err), interaction_id })
        }
      },
      None => {
        if setup {
          println!("    {}", Yellow.paint("WARNING: State Change ignored as there is no state change URL"));
        }
        Ok(hashmap!{})
      }
    }
  }
}