1use 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#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[allow(non_camel_case_types)]
27pub enum CatalogueEntryType {
28 CONTENT_MATCHER,
30 CONTENT_GENERATOR,
32 TRANSPORT,
34 MATCHER,
36 INTERACTION
38}
39
40impl CatalogueEntryType {
41 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#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
102#[allow(non_camel_case_types)]
103pub enum CatalogueEntryProviderType {
104 CORE,
106 PLUGIN
108}
109
110#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct CatalogueEntry {
114 pub entry_type: CatalogueEntryType,
116 pub provider_type: CatalogueEntryProviderType,
118 pub plugin: Option<PactPluginManifest>,
120 pub key: String,
122 pub values: HashMap<String, String>
124}
125
126pub 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
147pub 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
167pub 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#[derive(Debug, Clone)]
181pub enum ResolvedCapability {
182 Core(String),
184 Plugin(Box<PactPluginManifest>)
186}
187
188pub fn resolve_capability(entry_key: &str, expected_type: CatalogueEntryType) -> anyhow::Result<ResolvedCapability> {
204 let candidates: Vec<(String, CatalogueEntry)> = {
205 let inner = CATALOGUE_REGISTER.lock().unwrap();
206 inner.iter()
207 .filter(|(k, _)| k.ends_with(entry_key))
208 .map(|(k, v)| (k.clone(), v.clone()))
209 .collect()
210 };
211
212 let mut of_expected_type = candidates.iter().filter(|(_, entry)| entry.entry_type == expected_type);
213 let entry = match (of_expected_type.next(), of_expected_type.next()) {
214 (None, _) => return match candidates.first() {
215 Some((_, entry)) => Err(anyhow::anyhow!(
216 "Catalogue entry '{}' is a {:?}, not a {:?}", entry_key, entry.entry_type, expected_type
217 )),
218 None => Err(anyhow::anyhow!("No catalogue entry found for key '{}'", entry_key))
219 },
220 (Some(only), None) => &only.1,
221 (Some(first), Some(second)) => {
222 let mut keys: Vec<&str> = std::iter::once(first).chain(std::iter::once(second))
223 .chain(of_expected_type)
224 .map(|(k, _)| k.as_str())
225 .collect();
226 keys.sort_unstable();
227 return Err(anyhow::anyhow!(
228 "Ambiguous catalogue entry key '{}': matches multiple entries ({}) - register it under a more specific key",
229 entry_key, keys.join(", ")
230 ));
231 }
232 };
233
234 match entry.provider_type {
235 CatalogueEntryProviderType::CORE => Ok(ResolvedCapability::Core(entry.key.clone())),
236 CatalogueEntryProviderType::PLUGIN => entry.plugin.clone()
237 .map(|manifest| ResolvedCapability::Plugin(Box::new(manifest)))
238 .ok_or_else(|| anyhow::anyhow!("Catalogue entry '{}' has no plugin manifest", entry_key))
239 }
240}
241
242pub fn remove_plugin_entries(name: &str) {
244 trace!("remove_plugin_entries({})", name);
245
246 let prefix = format!("plugin/{}/", name);
247 let keys: Vec<String> = {
248 let guard = CATALOGUE_REGISTER.lock().unwrap();
249 guard.keys()
250 .filter(|key| key.starts_with(&prefix))
251 .cloned()
252 .collect()
253 };
254
255 let mut guard = CATALOGUE_REGISTER.lock().unwrap();
256 for key in keys {
257 guard.remove(&key);
258 }
259
260 debug!("Removed all catalogue entries for plugin {}", name);
261}
262
263#[instrument(level = "trace", skip(content_type))]
265pub fn find_content_matcher<CT: Into<String>>(content_type: CT) -> Option<ContentMatcher> {
266 let content_type_str = content_type.into();
267 debug!("Looking for a content matcher for {}", content_type_str);
268 let content_type = match ContentType::parse(content_type_str.as_str()) {
269 Ok(ct) => ct,
270 Err(err) => {
271 error!("'{}' is not a valid content type", err);
272 return None;
273 }
274 };
275 let guard = CATALOGUE_REGISTER.lock().unwrap();
276 trace!("Catalogue has {} entries", guard.len());
277 guard.values().find(|entry| {
278 trace!("Catalogue entry {:?}", entry);
279 if entry.entry_type == CatalogueEntryType::CONTENT_MATCHER {
280 trace!("Catalogue entry is a content matcher for {:?}", entry.values.get("content-types"));
281 if let Some(content_types) = entry.values.get("content-types") {
282 content_types.split(";").any(|ct| matches_pattern(ct.trim(), &content_type))
283 } else {
284 false
285 }
286 } else {
287 false
288 }
289 }).map(|entry| ContentMatcher { catalogue_entry: entry.clone() })
290}
291
292fn matches_pattern(pattern: &str, content_type: &ContentType) -> bool {
301 let base_type = match &content_type.suffix {
307 Some(suffix) => format!("{}/{}+{}", content_type.main_type, content_type.sub_type, suffix),
308 None => format!("{}/{}", content_type.main_type, content_type.sub_type)
309 };
310 match Regex::new(&format!("^(?:{})$", pattern)) {
311 Ok(regex) => regex.is_match(base_type.as_str()),
312 Err(err) => {
313 error!("Failed to parse '{}' as a regex - {}", pattern, err);
314 false
315 }
316 }
317}
318
319pub fn find_content_generator(content_type: &ContentType) -> Option<ContentGenerator> {
321 debug!("Looking for a content generator for {}", content_type);
322 let guard = CATALOGUE_REGISTER.lock().unwrap();
323 guard.values().find(|entry| {
324 if entry.entry_type == CatalogueEntryType::CONTENT_GENERATOR {
325 if let Some(content_types) = entry.values.get("content-types") {
326 content_types.split(";").any(|ct| matches_pattern(ct.trim(), content_type))
327 } else {
328 false
329 }
330 } else {
331 false
332 }
333 }).map(|entry| ContentGenerator { catalogue_entry: entry.clone() })
334}
335
336pub fn all_entries() -> Vec<CatalogueEntry> {
338 let guard = CATALOGUE_REGISTER.lock().unwrap();
339 guard.values().cloned().collect()
340}
341
342pub fn core_entries() -> Vec<CatalogueEntry> {
344 let guard = CATALOGUE_REGISTER.lock().unwrap();
345 guard.values()
346 .filter(|entry| entry.provider_type == CatalogueEntryProviderType::CORE)
347 .cloned()
348 .collect()
349}
350
351#[cfg(test)]
352mod tests {
353 use expectest::prelude::*;
354 use maplit::hashmap;
355
356 use crate::proto::catalogue_entry;
357
358 use super::*;
359
360 #[test]
361 fn sets_plugin_catalogue_entries_correctly() {
362 let manifest = PactPluginManifest {
364 name: "sets_plugin_catalogue_entries_correctly".to_string(),
365 .. PactPluginManifest::default()
366 };
367 let entries = vec![
368 ProtoCatalogueEntry {
369 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
370 key: "protobuf".to_string(),
371 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
372 },
373 ProtoCatalogueEntry {
374 r#type: catalogue_entry::EntryType::ContentGenerator as i32,
375 key: "protobuf".to_string(),
376 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
377 },
378 ProtoCatalogueEntry {
379 r#type: catalogue_entry::EntryType::Transport as i32,
380 key: "grpc".to_string(),
381 values: hashmap!{}
382 }
383 ];
384
385 register_plugin_entries(&manifest, &entries);
387
388 let matcher_entry = lookup_entry("content-matcher/protobuf");
390 let generator_entry = lookup_entry("content-generator/protobuf");
391 let transport_entry = lookup_entry("transport/grpc");
392
393 remove_plugin_entries("sets_plugin_catalogue_entries_correctly");
394
395 expect!(matcher_entry).to(be_some().value(CatalogueEntry {
396 entry_type: CatalogueEntryType::CONTENT_MATCHER,
397 provider_type: CatalogueEntryProviderType::PLUGIN,
398 plugin: Some(manifest.clone()),
399 key: "protobuf".to_string(),
400 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
401 }));
402 expect!(generator_entry).to(be_some().value(CatalogueEntry {
403 entry_type: CatalogueEntryType::CONTENT_GENERATOR,
404 provider_type: CatalogueEntryProviderType::PLUGIN,
405 plugin: Some(manifest.clone()),
406 key: "protobuf".to_string(),
407 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
408 }));
409 expect!(transport_entry).to(be_some().value(CatalogueEntry {
410 entry_type: CatalogueEntryType::TRANSPORT,
411 provider_type: CatalogueEntryProviderType::PLUGIN,
412 plugin: Some(manifest.clone()),
413 key: "grpc".to_string(),
414 values: hashmap!{}
415 }));
416 }
417
418 #[test]
419 fn find_content_matcher_requires_the_whole_base_type_to_match() {
420 let manifest = PactPluginManifest {
421 name: "find_content_matcher_requires_the_whole_base_type_to_match".to_string(),
422 .. PactPluginManifest::default()
423 };
424 let entries = vec![
425 ProtoCatalogueEntry {
426 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
427 key: "jwt".to_string(),
428 values: hashmap!{ "content-types".to_string() => "application/jwt;application/jwt\\+json".to_string() }
430 }
431 ];
432 register_plugin_entries(&manifest, &entries);
433
434 let exact_match = find_content_matcher("application/jwt+json");
435 let with_params = find_content_matcher("application/jwt+json;charset=utf-8");
436 let longer_type = find_content_matcher("application/jwt+jsonextra");
437 let unrelated_type = find_content_matcher("application/json");
438
439 remove_plugin_entries("find_content_matcher_requires_the_whole_base_type_to_match");
440
441 expect!(exact_match).to(be_some());
442 expect!(with_params).to(be_some());
443 expect!(longer_type).to(be_none());
444 expect!(unrelated_type).to(be_none());
445 }
446
447 #[test]
448 fn resolve_capability_resolves_an_unambiguous_core_entry() {
449 let key = "resolve_capability_resolves_an_unambiguous_core_entry";
450 register_core_entries(&vec![CatalogueEntry {
451 entry_type: CatalogueEntryType::CONTENT_MATCHER,
452 provider_type: CatalogueEntryProviderType::CORE,
453 plugin: None,
454 key: key.to_string(),
455 values: hashmap!{}
456 }]);
457
458 let resolved = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER).unwrap();
459
460 let core_key = match resolved {
461 ResolvedCapability::Core(core_key) => core_key,
462 ResolvedCapability::Plugin(_) => panic!("expected a Core resolution, got Plugin")
463 };
464 expect!(core_key).to(be_equal_to(key.to_string()));
465 }
466
467 #[test]
468 fn resolve_capability_returns_a_clear_error_for_an_unregistered_key() {
469 let result = resolve_capability(
470 "resolve_capability_returns_a_clear_error_for_an_unregistered_key",
471 CatalogueEntryType::CONTENT_MATCHER
472 );
473
474 let err = result.expect_err("expected an error for an unregistered key");
475 expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
476 }
477
478 #[test]
479 fn resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape() {
480 let key = "resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape";
481 register_core_entries(&vec![CatalogueEntry {
482 entry_type: CatalogueEntryType::CONTENT_GENERATOR,
483 provider_type: CatalogueEntryProviderType::CORE,
484 plugin: None,
485 key: key.to_string(),
486 values: hashmap!{}
487 }]);
488
489 let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
490
491 let err = result.expect_err("expected an error when the entry is a generator, not a matcher");
492 expect!(err.to_string().contains("is a CONTENT_GENERATOR, not a CONTENT_MATCHER")).to(be_true());
493 }
494
495 #[test]
496 fn resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry() {
497 let key = "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry";
498 let manifest = PactPluginManifest {
499 name: "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry".to_string(),
500 .. PactPluginManifest::default()
501 };
502 register_core_entries(&vec![CatalogueEntry {
503 entry_type: CatalogueEntryType::CONTENT_MATCHER,
504 provider_type: CatalogueEntryProviderType::CORE,
505 plugin: None,
506 key: key.to_string(),
507 values: hashmap!{}
508 }]);
509 register_plugin_entries(&manifest, &vec![ProtoCatalogueEntry {
510 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
511 key: key.to_string(),
512 values: hashmap!{}
513 }]);
514
515 let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
516
517 remove_plugin_entries(&manifest.name);
518
519 let err = result.expect_err("expected an error for a key matching more than one entry");
520 expect!(err.to_string().contains("Ambiguous catalogue entry key")).to(be_true());
521 }
522}