pact_plugin_driver/
catalogue_manager.rs

1//! Manages the catalogue of features provided by plugins
2
3use std::collections::HashMap;
4use std::fmt::{self, Display, Formatter};
5use std::sync::Mutex;
6
7use itertools::Itertools;
8use lazy_static::lazy_static;
9use maplit::hashset;
10use pact_models::content_types::ContentType;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use tracing::{debug, error, instrument, trace};
14
15use crate::content::{ContentGenerator, ContentMatcher};
16use crate::plugin_models::PactPluginManifest;
17use crate::proto::catalogue_entry::EntryType;
18use crate::proto::CatalogueEntry as ProtoCatalogueEntry;
19
20lazy_static! {
21  static ref CATALOGUE_REGISTER: Mutex<HashMap<String, CatalogueEntry>> = Mutex::new(HashMap::new());
22}
23
24/// Type of catalogue entry
25#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[allow(non_camel_case_types)]
27pub enum CatalogueEntryType {
28  /// Content matcher (based on content type)
29  CONTENT_MATCHER,
30  /// Content generator (based on content type)
31  CONTENT_GENERATOR,
32  /// Network transport
33  TRANSPORT,
34  /// Matching rule
35  MATCHER,
36  /// Generator
37  INTERACTION
38}
39
40impl CatalogueEntryType {
41  /// Return the protobuf type for this entry type
42  pub fn to_proto_type(&self) -> EntryType {
43    match self {
44      CatalogueEntryType::CONTENT_MATCHER => EntryType::ContentMatcher,
45      CatalogueEntryType::CONTENT_GENERATOR => EntryType::ContentGenerator,
46      CatalogueEntryType::TRANSPORT => EntryType::Transport,
47      CatalogueEntryType::MATCHER => EntryType::Matcher,
48      CatalogueEntryType::INTERACTION => EntryType::Interaction
49    }
50  }
51}
52
53impl Display for CatalogueEntryType {
54  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55    match self {
56      CatalogueEntryType::CONTENT_MATCHER => write!(f, "content-matcher"),
57      CatalogueEntryType::CONTENT_GENERATOR => write!(f, "content-generator"),
58      CatalogueEntryType::TRANSPORT => write!(f, "transport"),
59      CatalogueEntryType::MATCHER => write!(f, "matcher"),
60      CatalogueEntryType::INTERACTION => write!(f, "interaction"),
61    }
62  }
63}
64
65impl From<&str> for CatalogueEntryType {
66  fn from(s: &str) -> Self {
67    match s {
68      "content-matcher" => CatalogueEntryType::CONTENT_MATCHER,
69      "content-generator" => CatalogueEntryType::CONTENT_GENERATOR,
70      "interaction" => CatalogueEntryType::INTERACTION,
71      "matcher" => CatalogueEntryType::MATCHER,
72      "transport" => CatalogueEntryType::TRANSPORT,
73      _ => {
74        let message = format!("'{}' is not a valid CatalogueEntryType value", s);
75        error!("{}", message);
76        panic!("{}", message)
77      }
78    }
79  }
80}
81
82impl From<String> for CatalogueEntryType {
83  fn from(s: String) -> Self {
84    Self::from(s.as_str())
85  }
86}
87
88impl From<EntryType> for CatalogueEntryType {
89  fn from(t: EntryType) -> Self {
90    match t {
91      EntryType::ContentMatcher => CatalogueEntryType::CONTENT_MATCHER,
92      EntryType::ContentGenerator => CatalogueEntryType::CONTENT_GENERATOR,
93      EntryType::Transport => CatalogueEntryType::TRANSPORT,
94      EntryType::Matcher => CatalogueEntryType::MATCHER,
95      EntryType::Interaction => CatalogueEntryType::INTERACTION
96    }
97  }
98}
99
100/// Provider of the catalogue entry
101#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
102#[allow(non_camel_case_types)]
103pub enum CatalogueEntryProviderType {
104  /// Core Pact framework
105  CORE,
106  /// Plugin
107  PLUGIN
108}
109
110/// Catalogue entry
111#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct CatalogueEntry {
114  /// Type of entry
115  pub entry_type: CatalogueEntryType,
116  /// Provider of the entry
117  pub provider_type: CatalogueEntryProviderType,
118  /// Plugin manifest
119  pub plugin: Option<PactPluginManifest>,
120  /// Entry key
121  pub key: String,
122  /// assocaited Entry values
123  pub values: HashMap<String, String>
124}
125
126/// Register the entries in the global catalogue
127pub fn register_plugin_entries(plugin: &PactPluginManifest, catalogue_list: &Vec<ProtoCatalogueEntry>) {
128  trace!("register_plugin_entries({:?}, {:?})", plugin, catalogue_list);
129
130  let mut guard = CATALOGUE_REGISTER.lock().unwrap();
131
132  for entry in catalogue_list {
133    let entry_type = CatalogueEntryType::from(entry.r#type());
134    let key = format!("plugin/{}/{}/{}", plugin.name, entry_type, entry.key);
135    guard.insert(key.clone(), CatalogueEntry {
136      entry_type,
137      provider_type: CatalogueEntryProviderType::PLUGIN,
138      plugin: Some(plugin.clone()),
139      key: entry.key.clone(),
140      values: entry.values.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
141    });
142  }
143
144  debug!("Updated catalogue entries:\n{}", guard.keys().sorted().join("\n"))
145}
146
147/// Register the core Pact framework entries in the global catalogue
148pub fn register_core_entries(entries: &Vec<CatalogueEntry>) {
149  trace!("register_core_entries({:?})", entries);
150
151  let mut inner = CATALOGUE_REGISTER.lock().unwrap();
152
153  let mut updated_keys = hashset!();
154  for entry in entries {
155    let key = format!("core/{}/{}", entry.entry_type, entry.key);
156    if !inner.contains_key(&key) {
157      inner.insert(key.clone(), entry.clone());
158      updated_keys.insert(key.clone());
159    }
160  }
161
162  if !updated_keys.is_empty() {
163    debug!("Updated catalogue entries:\n{}", updated_keys.iter().sorted().join("\n"));
164  }
165}
166
167/// Lookup an entry in the catalogue by the key. Will find the first entry that ends with the
168/// given key.
169pub fn lookup_entry(key: &str) -> Option<CatalogueEntry> {
170  let inner = CATALOGUE_REGISTER.lock().unwrap();
171  inner.iter()
172    .find(|(k, _)| k.ends_with(key))
173    .map(|(_, v)| v.clone())
174}
175
176/// Remove all entries for a plugin given the plugin name
177pub fn remove_plugin_entries(name: &str) {
178  trace!("remove_plugin_entries({})", name);
179
180  let prefix = format!("plugin/{}/", name);
181  let keys: Vec<String> = {
182    let guard = CATALOGUE_REGISTER.lock().unwrap();
183    guard.keys()
184      .filter(|key| key.starts_with(&prefix))
185      .cloned()
186      .collect()
187  };
188
189  let mut guard = CATALOGUE_REGISTER.lock().unwrap();
190  for key in keys {
191    guard.remove(&key);
192  }
193
194  debug!("Removed all catalogue entries for plugin {}", name);
195}
196
197/// Find a content matcher in the global catalogue for the provided content type
198#[instrument(level = "trace", skip(content_type))]
199pub fn find_content_matcher<CT: Into<String>>(content_type: CT) -> Option<ContentMatcher> {
200  let content_type_str = content_type.into();
201  debug!("Looking for a content matcher for {}", content_type_str);
202  let content_type = match ContentType::parse(content_type_str.as_str()) {
203    Ok(ct) => ct,
204    Err(err) => {
205      error!("'{}' is not a valid content type", err);
206      return None;
207    }
208  };
209  let guard = CATALOGUE_REGISTER.lock().unwrap();
210  trace!("Catalogue has {} entries", guard.len());
211  guard.values().find(|entry| {
212    trace!("Catalogue entry {:?}", entry);
213    if entry.entry_type == CatalogueEntryType::CONTENT_MATCHER {
214      trace!("Catalogue entry is a content matcher for {:?}", entry.values.get("content-types"));
215      if let Some(content_types) = entry.values.get("content-types") {
216        content_types.split(";").any(|ct| matches_pattern(ct.trim(), &content_type))
217      } else {
218        false
219      }
220    } else {
221      false
222    }
223  }).map(|entry| ContentMatcher { catalogue_entry: entry.clone() })
224}
225
226fn matches_pattern(pattern: &str, content_type: &ContentType) -> bool {
227  let base_type = content_type.base_type().to_string();
228  match Regex::new(pattern) {
229    Ok(regex) => regex.is_match(content_type.to_string().as_str()) || regex.is_match(base_type.as_str()),
230    Err(err) => {
231      error!("Failed to parse '{}' as a regex - {}", pattern, err);
232      false
233    }
234  }
235}
236
237/// Find a content generator in the global catalogue for the provided content type
238pub fn find_content_generator(content_type: &ContentType) -> Option<ContentGenerator> {
239  debug!("Looking for a content generator for {}", content_type);
240  let guard = CATALOGUE_REGISTER.lock().unwrap();
241  guard.values().find(|entry| {
242    if entry.entry_type == CatalogueEntryType::CONTENT_GENERATOR {
243      if let Some(content_types) = entry.values.get("content-types") {
244        content_types.split(";").any(|ct| matches_pattern(ct.trim(), content_type))
245      } else {
246        false
247      }
248    } else {
249      false
250    }
251  }).map(|entry| ContentGenerator { catalogue_entry: entry.clone() })
252}
253
254/// Returns a copy of all catalogue entries
255pub fn all_entries() -> Vec<CatalogueEntry> {
256  let guard = CATALOGUE_REGISTER.lock().unwrap();
257  guard.values().cloned().collect()
258}
259
260#[cfg(test)]
261mod tests {
262  use expectest::prelude::*;
263  use maplit::hashmap;
264
265  use crate::proto::catalogue_entry;
266
267  use super::*;
268
269  #[test]
270  fn sets_plugin_catalogue_entries_correctly() {
271    // Given
272    let manifest = PactPluginManifest {
273      name: "sets_plugin_catalogue_entries_correctly".to_string(),
274      .. PactPluginManifest::default()
275    };
276    let entries = vec![
277      ProtoCatalogueEntry {
278        r#type: catalogue_entry::EntryType::ContentMatcher as i32,
279        key: "protobuf".to_string(),
280        values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
281      },
282      ProtoCatalogueEntry {
283        r#type: catalogue_entry::EntryType::ContentGenerator as i32,
284        key: "protobuf".to_string(),
285        values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
286      },
287      ProtoCatalogueEntry {
288        r#type: catalogue_entry::EntryType::Transport as i32,
289        key: "grpc".to_string(),
290        values: hashmap!{}
291      }
292    ];
293
294    // When
295    register_plugin_entries(&manifest, &entries);
296
297    // Then
298    let matcher_entry = lookup_entry("content-matcher/protobuf");
299    let generator_entry = lookup_entry("content-generator/protobuf");
300    let transport_entry = lookup_entry("transport/grpc");
301
302    remove_plugin_entries("sets_plugin_catalogue_entries_correctly");
303
304    expect!(matcher_entry).to(be_some().value(CatalogueEntry {
305      entry_type: CatalogueEntryType::CONTENT_MATCHER,
306      provider_type: CatalogueEntryProviderType::PLUGIN,
307      plugin: Some(manifest.clone()),
308      key: "protobuf".to_string(),
309      values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
310    }));
311    expect!(generator_entry).to(be_some().value(CatalogueEntry {
312      entry_type: CatalogueEntryType::CONTENT_GENERATOR,
313      provider_type: CatalogueEntryProviderType::PLUGIN,
314      plugin: Some(manifest.clone()),
315      key: "protobuf".to_string(),
316      values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
317    }));
318    expect!(transport_entry).to(be_some().value(CatalogueEntry {
319      entry_type: CatalogueEntryType::TRANSPORT,
320      provider_type: CatalogueEntryProviderType::PLUGIN,
321      plugin: Some(manifest.clone()),
322      key: "grpc".to_string(),
323      values: hashmap!{}
324    }));
325  }
326}