pact_models/
provider_states.rs

1//! `provider_states` module contains all the logic for dealing with provider states.
2//! See `https://docs.pact.io/getting_started/provider_states` for more info on provider states.
3
4use std::cmp::Eq;
5use std::collections::HashMap;
6use std::hash::{Hash, Hasher};
7
8use maplit::*;
9use serde::{Deserialize, Serialize};
10use serde_json::*;
11use tracing::warn;
12
13use crate::PactSpecification;
14use crate::verify_json::{json_type_of, PactFileVerificationResult, PactJsonVerifier, ResultLevel};
15
16/// Struct that encapsulates all the info about a provider state
17#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
18pub struct ProviderState {
19  /// Description of this provider state
20  pub name: String,
21  /// Provider state parameters as key value pairs
22  pub params: HashMap<String, Value>
23}
24
25impl ProviderState {
26
27  /// Creates a default state with the given name
28  pub fn default<T: Into<String>>(name: T) -> ProviderState {
29    ProviderState {
30      name: name.into(),
31      params: hashmap!{}
32    }
33  }
34
35  /// Constructs a provider state from the `Json` struct
36  pub fn from_json_v3(pact_json: &Value) -> ProviderState {
37    let state = match pact_json.get("name") {
38      Some(v) => match *v {
39        Value::String(ref s) => s.clone(),
40        _ => v.to_string()
41      },
42      None => {
43        warn!("Provider state does not have a 'name' field");
44        "unknown provider states".to_string()
45      }
46    };
47    let params = match pact_json.get("params") {
48      Some(v) => match *v {
49        Value::Object(ref map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
50        _ => {
51          warn!("Provider state parameters must be a map");
52          hashmap!{}
53        }
54      },
55      None => hashmap!{}
56    };
57    ProviderState{
58      name: state,
59      params
60    }
61  }
62
63  /// Constructs a list of provider states from the `Json` struct
64  pub fn from_json(pact_json: &Value) -> Vec<ProviderState> {
65    match pact_json.get("providerStates") {
66      Some(v) => match *v {
67        Value::Array(ref a) => a.iter().map(|i| ProviderState::from_json_v3(i)).collect(),
68        _ => vec![]
69      },
70      None => match pact_json.get("providerState").or_else(|| pact_json.get("provider_state")) {
71        Some(v) => match *v {
72          Value::String(ref s) => if s.is_empty() {
73            vec![]
74          } else {
75            vec![ProviderState{ name: s.clone(), params: hashmap!{} }]
76          },
77          Value::Null => vec![],
78          _ => vec![ProviderState{ name: v.to_string(), params: hashmap!{} }]
79        },
80        None => vec![]
81      }
82    }
83  }
84
85  /// Converts this provider state into a JSON structure
86  pub fn to_json(&self) -> Value {
87    let mut value = json!({
88            "name": Value::String(self.name.clone())
89        });
90    if !self.params.is_empty() {
91      let map = value.as_object_mut().unwrap();
92      map.insert("params".into(), Value::Object(
93        self.params.iter().map(|(k, v)| (k.clone(), v.clone())).collect()));
94    }
95    value
96  }
97
98}
99
100impl Hash for ProviderState {
101  fn hash<H: Hasher>(&self, state: &mut H) {
102    self.name.hash(state);
103    for (k, v) in self.params.clone() {
104      k.hash(state);
105      match v {
106        Value::Number(n) => if n.is_u64() {
107          n.as_u64().unwrap().hash(state)
108        } else if n.is_i64() {
109          n.as_i64().unwrap().hash(state)
110        } else if n.is_f64() {
111          n.as_f64().unwrap().to_string().hash(state)
112        },
113        Value::String(s) => s.hash(state),
114        Value::Bool(b) => b.hash(state),
115        _ => ()
116      }
117    }
118  }
119}
120
121impl PartialEq for ProviderState {
122  fn eq(&self, other: &Self) -> bool {
123    self.name == other.name && self.params == other.params
124  }
125}
126
127impl PactJsonVerifier for ProviderState {
128  fn verify_json(path: &str, pact_json: &Value, strict: bool, _spec_version: PactSpecification) -> Vec<PactFileVerificationResult> {
129    let mut results = vec![];
130
131    match pact_json {
132      Value::String(_) => {}
133      Value::Object(values) => {
134        match values.get("name") {
135          None => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
136            "Provider state 'name' is required")),
137          Some(name) => if !name.is_string() {
138            results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
139              format!("Provider state 'name' must be a String, got {}", json_type_of(pact_json))))
140          }
141        }
142
143        if let Some(params) = values.get("params") {
144          if !params.is_object() {
145            results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
146              format!("Provider state 'params' must be an Object, got {}", json_type_of(pact_json))))
147          }
148        }
149
150        let valid_attr = hashset! { "name", "params" };
151        for key in values.keys() {
152          if !valid_attr.contains(key.as_str()) {
153            results.push(PactFileVerificationResult::new(path.to_owned(),
154              if strict { ResultLevel::ERROR } else { ResultLevel::WARNING }, format!("Unknown attribute '{}'", key)))
155          }
156        }
157      }
158      _ => results.push(PactFileVerificationResult::new(path, ResultLevel::ERROR,
159        format!("Must be a String or Object, got {}", json_type_of(pact_json))))
160    }
161
162    results
163  }
164}
165
166#[cfg(test)]
167mod tests {
168  use expectest::expect;
169  use expectest::prelude::*;
170  use serde_json;
171  use serde_json::Value;
172
173  use super::*;
174
175  #[test]
176  fn defaults_to_v3_pact_provider_states() {
177    let json = r#"{
178            "providerStates": [
179              {
180                "name": "test state",
181                "params": { "name": "Testy" }
182              },
183              {
184                "name": "test state 2",
185                "params": { "name": "Testy2" }
186              }
187            ],
188            "description" : "test interaction"
189        }"#;
190    let provider_states = ProviderState::from_json(&serde_json::from_str(json).unwrap());
191    expect!(provider_states.iter()).to(have_count(2));
192    expect!(&provider_states[0]).to(be_equal_to(&ProviderState {
193      name: "test state".into(),
194      params: hashmap!{ "name".to_string() => Value::String("Testy".into()) }
195    }));
196    expect!(&provider_states[1]).to(be_equal_to(&ProviderState {
197      name: "test state 2".into(),
198      params: hashmap!{ "name".to_string() => Value::String("Testy2".into()) }
199    }));
200  }
201
202  #[test]
203  fn falls_back_to_v2_pact_provider_state() {
204    let json = r#"{
205            "providerState": "test state",
206            "description" : "test interaction"
207        }"#;
208    let provider_states = ProviderState::from_json(&serde_json::from_str(json).unwrap());
209    expect!(provider_states.iter()).to(have_count(1));
210    expect!(&provider_states[0]).to(be_equal_to(&ProviderState {
211      name: "test state".to_string(),
212      params: hashmap!{}
213    }));
214  }
215
216  #[test]
217  fn pact_with_no_provider_states() {
218    let json = r#"{
219            "description" : "test interaction"
220        }"#;
221    let provider_states = ProviderState::from_json(&serde_json::from_str(json).unwrap());
222    expect!(provider_states.iter()).to(be_empty());
223  }
224}