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, warn};
14
15use crate::content::{ContentGenerator, ContentMatcher};
16use crate::plugin_models::PactPluginManifest;
17use crate::proto::catalogue_entry::EntryType;
18use crate::proto::CatalogueEntry as ProtoCatalogueEntry;
19use crate::proto_v2::catalogue_entry::EntryType as EntryTypeV2;
20
21lazy_static! {
22 static ref CATALOGUE_REGISTER: Mutex<HashMap<String, CatalogueEntry>> = Mutex::new(HashMap::new());
23}
24
25#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
27#[allow(non_camel_case_types)]
28pub enum CatalogueEntryType {
29 CONTENT_MATCHER,
31 CONTENT_GENERATOR,
33 TRANSPORT,
35 MATCHER,
37 INTERACTION,
39 GENERATOR
42}
43
44impl CatalogueEntryType {
45 #[deprecated(
51 since = "1.2.0",
52 note = "Use to_proto_value, which can represent entry types that only exist on the V2 interface"
53 )]
54 pub fn to_proto_type(&self) -> EntryType {
55 match self {
56 CatalogueEntryType::CONTENT_MATCHER => EntryType::ContentMatcher,
57 CatalogueEntryType::CONTENT_GENERATOR => EntryType::ContentGenerator,
58 CatalogueEntryType::TRANSPORT => EntryType::Transport,
59 CatalogueEntryType::MATCHER => EntryType::Matcher,
60 CatalogueEntryType::INTERACTION => EntryType::Interaction,
61 CatalogueEntryType::GENERATOR => EntryType::Matcher
62 }
63 }
64
65 fn to_proto_enum(self) -> EntryTypeV2 {
68 match self {
69 CatalogueEntryType::CONTENT_MATCHER => EntryTypeV2::ContentMatcher,
70 CatalogueEntryType::CONTENT_GENERATOR => EntryTypeV2::ContentGenerator,
71 CatalogueEntryType::TRANSPORT => EntryTypeV2::Transport,
72 CatalogueEntryType::MATCHER => EntryTypeV2::Matcher,
73 CatalogueEntryType::INTERACTION => EntryTypeV2::Interaction,
74 CatalogueEntryType::GENERATOR => EntryTypeV2::Generator
75 }
76 }
77
78 fn from_proto_enum(entry_type: EntryTypeV2) -> CatalogueEntryType {
79 match entry_type {
80 EntryTypeV2::ContentMatcher => CatalogueEntryType::CONTENT_MATCHER,
81 EntryTypeV2::ContentGenerator => CatalogueEntryType::CONTENT_GENERATOR,
82 EntryTypeV2::Transport => CatalogueEntryType::TRANSPORT,
83 EntryTypeV2::Matcher => CatalogueEntryType::MATCHER,
84 EntryTypeV2::Interaction => CatalogueEntryType::INTERACTION,
85 EntryTypeV2::Generator => CatalogueEntryType::GENERATOR
86 }
87 }
88
89 pub fn to_proto_value(self) -> i32 {
95 self.to_proto_enum() as i32
96 }
97
98 pub fn from_proto_value(value: i32) -> Option<CatalogueEntryType> {
103 EntryTypeV2::try_from(value).ok().map(CatalogueEntryType::from_proto_enum)
104 }
105
106 pub fn as_proto_name(&self) -> &'static str {
109 self.to_proto_enum().as_str_name()
110 }
111
112 pub fn from_proto_name(name: &str) -> Option<CatalogueEntryType> {
115 EntryTypeV2::from_str_name(name).map(CatalogueEntryType::from_proto_enum)
116 }
117}
118
119impl Display for CatalogueEntryType {
120 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
121 match self {
122 CatalogueEntryType::CONTENT_MATCHER => write!(f, "content-matcher"),
123 CatalogueEntryType::CONTENT_GENERATOR => write!(f, "content-generator"),
124 CatalogueEntryType::TRANSPORT => write!(f, "transport"),
125 CatalogueEntryType::MATCHER => write!(f, "matcher"),
126 CatalogueEntryType::INTERACTION => write!(f, "interaction"),
127 CatalogueEntryType::GENERATOR => write!(f, "generator"),
128 }
129 }
130}
131
132impl From<&str> for CatalogueEntryType {
133 fn from(s: &str) -> Self {
134 match s {
135 "content-matcher" => CatalogueEntryType::CONTENT_MATCHER,
136 "content-generator" => CatalogueEntryType::CONTENT_GENERATOR,
137 "interaction" => CatalogueEntryType::INTERACTION,
138 "matcher" => CatalogueEntryType::MATCHER,
139 "transport" => CatalogueEntryType::TRANSPORT,
140 "generator" => CatalogueEntryType::GENERATOR,
141 _ => {
142 let message = format!("'{}' is not a valid CatalogueEntryType value", s);
143 error!("{}", message);
144 panic!("{}", message)
145 }
146 }
147 }
148}
149
150impl From<String> for CatalogueEntryType {
151 fn from(s: String) -> Self {
152 Self::from(s.as_str())
153 }
154}
155
156impl From<EntryType> for CatalogueEntryType {
157 fn from(t: EntryType) -> Self {
158 match t {
159 EntryType::ContentMatcher => CatalogueEntryType::CONTENT_MATCHER,
160 EntryType::ContentGenerator => CatalogueEntryType::CONTENT_GENERATOR,
161 EntryType::Transport => CatalogueEntryType::TRANSPORT,
162 EntryType::Matcher => CatalogueEntryType::MATCHER,
163 EntryType::Interaction => CatalogueEntryType::INTERACTION
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
170#[allow(non_camel_case_types)]
171pub enum CatalogueEntryProviderType {
172 CORE,
174 PLUGIN
176}
177
178#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
180#[serde(rename_all = "camelCase")]
181pub struct CatalogueEntry {
182 pub entry_type: CatalogueEntryType,
184 pub provider_type: CatalogueEntryProviderType,
186 pub plugin: Option<PactPluginManifest>,
188 pub key: String,
190 pub values: HashMap<String, String>
192}
193
194pub fn register_plugin_entries(plugin: &PactPluginManifest, catalogue_list: &Vec<ProtoCatalogueEntry>) {
196 trace!("register_plugin_entries({:?}, {:?})", plugin, catalogue_list);
197
198 let mut guard = CATALOGUE_REGISTER.lock().unwrap();
199
200 for entry in catalogue_list {
201 let entry_type = match CatalogueEntryType::from_proto_value(entry.r#type) {
206 Some(entry_type) => entry_type,
207 None => {
208 warn!(
209 "Ignoring catalogue entry '{}' from plugin '{}': {} is not a catalogue entry type this driver understands",
210 entry.key, plugin.name, entry.r#type
211 );
212 continue;
213 }
214 };
215 let key = format!("plugin/{}/{}/{}", plugin.name, entry_type, entry.key);
216 guard.insert(key.clone(), CatalogueEntry {
217 entry_type,
218 provider_type: CatalogueEntryProviderType::PLUGIN,
219 plugin: Some(plugin.clone()),
220 key: entry.key.clone(),
221 values: entry.values.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
222 });
223 }
224
225 debug!("Updated catalogue entries:\n{}", guard.keys().sorted().join("\n"))
226}
227
228pub fn register_core_entries(entries: &Vec<CatalogueEntry>) {
230 trace!("register_core_entries({:?})", entries);
231
232 let mut inner = CATALOGUE_REGISTER.lock().unwrap();
233
234 let mut updated_keys = hashset!();
235 for entry in entries {
236 let key = format!("core/{}/{}", entry.entry_type, entry.key);
237 if !inner.contains_key(&key) {
238 inner.insert(key.clone(), entry.clone());
239 updated_keys.insert(key.clone());
240 }
241 }
242
243 if !updated_keys.is_empty() {
244 debug!("Updated catalogue entries:\n{}", updated_keys.iter().sorted().join("\n"));
245 }
246}
247
248pub fn lookup_entry(key: &str) -> Option<CatalogueEntry> {
256 let inner = CATALOGUE_REGISTER.lock().unwrap();
257 inner.iter()
258 .find(|(k, _)| names_catalogue_key(k, key))
259 .or_else(|| inner.iter().find(|(_, entry)| names_versioned_core_key(entry, key)))
260 .map(|(_, v)| v.clone())
261}
262
263#[derive(Debug, Clone)]
268pub enum ResolvedCapability {
269 Core(String),
271 Plugin(Box<PactPluginManifest>)
273}
274
275fn names_catalogue_key(catalogue_key: &str, entry_key: &str) -> bool {
285 let key_parts = catalogue_key.split('/').collect::<Vec<_>>();
286 let query_parts = entry_key.split('/').collect::<Vec<_>>();
287 query_parts.len() <= key_parts.len()
288 && key_parts[key_parts.len() - query_parts.len()..] == query_parts[..]
289}
290
291fn names_versioned_core_key(entry: &CatalogueEntry, entry_key: &str) -> bool {
304 if entry.entry_type != CatalogueEntryType::MATCHER && entry.entry_type != CatalogueEntryType::GENERATOR {
305 return false;
306 }
307 match entry.key.split_once('-') {
308 Some((version, name)) => name == entry_key
309 && version.len() > 1
310 && version.starts_with('v')
311 && version[1..].chars().all(|c| c.is_ascii_digit()),
312 None => false
313 }
314}
315
316pub fn resolve_capability(entry_key: &str, expected_type: CatalogueEntryType) -> anyhow::Result<ResolvedCapability> {
340 let entry = resolve_capability_entry(entry_key, expected_type)?;
341 match entry.provider_type {
342 CatalogueEntryProviderType::CORE => Ok(ResolvedCapability::Core(entry.key.clone())),
343 CatalogueEntryProviderType::PLUGIN => entry.plugin.clone()
344 .map(|manifest| ResolvedCapability::Plugin(Box::new(manifest)))
345 .ok_or_else(|| anyhow::anyhow!("Catalogue entry '{}' has no plugin manifest", entry_key))
346 }
347}
348
349pub fn resolve_capability_entry(entry_key: &str, expected_type: CatalogueEntryType) -> anyhow::Result<CatalogueEntry> {
354 let all_entries: Vec<(String, CatalogueEntry)> = {
355 let inner = CATALOGUE_REGISTER.lock().unwrap();
356 inner.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
357 };
358
359 let named: Vec<(String, CatalogueEntry)> = all_entries.iter()
360 .filter(|(k, _)| names_catalogue_key(k, entry_key))
361 .cloned()
362 .collect();
363 let candidates = if named.iter().any(|(_, entry)| entry.entry_type == expected_type) {
364 named
365 } else {
366 let versioned: Vec<(String, CatalogueEntry)> = all_entries.iter()
367 .filter(|(_, entry)| names_versioned_core_key(entry, entry_key))
368 .cloned()
369 .collect();
370 if versioned.is_empty() { named } else { versioned }
372 };
373
374 let mut of_expected_type = candidates.iter().filter(|(_, entry)| entry.entry_type == expected_type);
375 let entry = match (of_expected_type.next(), of_expected_type.next()) {
376 (None, _) => return match candidates.first() {
377 Some((_, entry)) => Err(anyhow::anyhow!(
378 "Catalogue entry '{}' is a {:?}, not a {:?}", entry_key, entry.entry_type, expected_type
379 )),
380 None => Err(anyhow::anyhow!("No catalogue entry found for key '{}'", entry_key))
381 },
382 (Some(only), None) => &only.1,
383 (Some(first), Some(second)) => {
384 let mut keys: Vec<&str> = std::iter::once(first).chain(std::iter::once(second))
385 .chain(of_expected_type)
386 .map(|(k, _)| k.as_str())
387 .collect();
388 keys.sort_unstable();
389 return Err(anyhow::anyhow!(
390 "Ambiguous catalogue entry key '{}': matches multiple entries ({}) - register it under a more specific key",
391 entry_key, keys.join(", ")
392 ));
393 }
394 };
395
396 Ok(entry.clone())
397}
398
399pub fn remove_plugin_entries(name: &str) {
401 trace!("remove_plugin_entries({})", name);
402
403 let prefix = format!("plugin/{}/", name);
404 let keys: Vec<String> = {
405 let guard = CATALOGUE_REGISTER.lock().unwrap();
406 guard.keys()
407 .filter(|key| key.starts_with(&prefix))
408 .cloned()
409 .collect()
410 };
411
412 let mut guard = CATALOGUE_REGISTER.lock().unwrap();
413 for key in keys {
414 guard.remove(&key);
415 }
416
417 debug!("Removed all catalogue entries for plugin {}", name);
418}
419
420#[instrument(level = "trace", skip(content_type))]
422pub fn find_content_matcher<CT: Into<String>>(content_type: CT) -> Option<ContentMatcher> {
423 let content_type_str = content_type.into();
424 debug!("Looking for a content matcher for {}", content_type_str);
425 let content_type = match ContentType::parse(content_type_str.as_str()) {
426 Ok(ct) => ct,
427 Err(err) => {
428 error!("'{}' is not a valid content type", err);
429 return None;
430 }
431 };
432 let guard = CATALOGUE_REGISTER.lock().unwrap();
433 trace!("Catalogue has {} entries", guard.len());
434 guard.values().find(|entry| {
435 trace!("Catalogue entry {:?}", entry);
436 if entry.entry_type == CatalogueEntryType::CONTENT_MATCHER {
437 trace!("Catalogue entry is a content matcher for {:?}", entry.values.get("content-types"));
438 if let Some(content_types) = entry.values.get("content-types") {
439 content_types.split(";").any(|ct| matches_pattern(ct.trim(), &content_type))
440 } else {
441 false
442 }
443 } else {
444 false
445 }
446 }).map(|entry| ContentMatcher { catalogue_entry: entry.clone() })
447}
448
449fn matches_pattern(pattern: &str, content_type: &ContentType) -> bool {
458 let base_type = match &content_type.suffix {
464 Some(suffix) => format!("{}/{}+{}", content_type.main_type, content_type.sub_type, suffix),
465 None => format!("{}/{}", content_type.main_type, content_type.sub_type)
466 };
467 match Regex::new(&format!("^(?:{})$", pattern)) {
468 Ok(regex) => regex.is_match(base_type.as_str()),
469 Err(err) => {
470 error!("Failed to parse '{}' as a regex - {}", pattern, err);
471 false
472 }
473 }
474}
475
476pub fn find_content_generator(content_type: &ContentType) -> Option<ContentGenerator> {
478 debug!("Looking for a content generator for {}", content_type);
479 let guard = CATALOGUE_REGISTER.lock().unwrap();
480 guard.values().find(|entry| {
481 if entry.entry_type == CatalogueEntryType::CONTENT_GENERATOR {
482 if let Some(content_types) = entry.values.get("content-types") {
483 content_types.split(";").any(|ct| matches_pattern(ct.trim(), content_type))
484 } else {
485 false
486 }
487 } else {
488 false
489 }
490 }).map(|entry| ContentGenerator { catalogue_entry: entry.clone() })
491}
492
493pub fn all_entries() -> Vec<CatalogueEntry> {
495 let guard = CATALOGUE_REGISTER.lock().unwrap();
496 guard.values().cloned().collect()
497}
498
499pub fn core_entries() -> Vec<CatalogueEntry> {
501 let guard = CATALOGUE_REGISTER.lock().unwrap();
502 guard.values()
503 .filter(|entry| entry.provider_type == CatalogueEntryProviderType::CORE)
504 .cloned()
505 .collect()
506}
507
508#[cfg(test)]
509mod tests {
510 use expectest::prelude::*;
511 use maplit::hashmap;
512
513 use crate::proto::catalogue_entry;
514
515 use super::*;
516
517 #[test]
518 fn sets_plugin_catalogue_entries_correctly() {
519 let manifest = PactPluginManifest {
521 name: "sets_plugin_catalogue_entries_correctly".to_string(),
522 .. PactPluginManifest::default()
523 };
524 let entries = vec![
525 ProtoCatalogueEntry {
526 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
527 key: "protobuf".to_string(),
528 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
529 },
530 ProtoCatalogueEntry {
531 r#type: catalogue_entry::EntryType::ContentGenerator as i32,
532 key: "protobuf".to_string(),
533 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
534 },
535 ProtoCatalogueEntry {
536 r#type: catalogue_entry::EntryType::Transport as i32,
537 key: "grpc".to_string(),
538 values: hashmap!{}
539 }
540 ];
541
542 register_plugin_entries(&manifest, &entries);
544
545 let matcher_entry = lookup_entry("content-matcher/protobuf");
547 let generator_entry = lookup_entry("content-generator/protobuf");
548 let transport_entry = lookup_entry("transport/grpc");
549
550 remove_plugin_entries("sets_plugin_catalogue_entries_correctly");
551
552 expect!(matcher_entry).to(be_some().value(CatalogueEntry {
553 entry_type: CatalogueEntryType::CONTENT_MATCHER,
554 provider_type: CatalogueEntryProviderType::PLUGIN,
555 plugin: Some(manifest.clone()),
556 key: "protobuf".to_string(),
557 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
558 }));
559 expect!(generator_entry).to(be_some().value(CatalogueEntry {
560 entry_type: CatalogueEntryType::CONTENT_GENERATOR,
561 provider_type: CatalogueEntryProviderType::PLUGIN,
562 plugin: Some(manifest.clone()),
563 key: "protobuf".to_string(),
564 values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
565 }));
566 expect!(transport_entry).to(be_some().value(CatalogueEntry {
567 entry_type: CatalogueEntryType::TRANSPORT,
568 provider_type: CatalogueEntryProviderType::PLUGIN,
569 plugin: Some(manifest.clone()),
570 key: "grpc".to_string(),
571 values: hashmap!{}
572 }));
573 }
574
575 #[test]
576 fn entry_type_proto_values_and_names_round_trip() {
577 for entry_type in [
578 CatalogueEntryType::CONTENT_MATCHER,
579 CatalogueEntryType::CONTENT_GENERATOR,
580 CatalogueEntryType::TRANSPORT,
581 CatalogueEntryType::MATCHER,
582 CatalogueEntryType::INTERACTION,
583 CatalogueEntryType::GENERATOR
584 ] {
585 expect!(CatalogueEntryType::from_proto_value(entry_type.to_proto_value()))
586 .to(be_some().value(entry_type));
587 expect!(CatalogueEntryType::from_proto_name(entry_type.as_proto_name()))
588 .to(be_some().value(entry_type));
589 expect!(CatalogueEntryType::from(entry_type.to_string().as_str())).to(be_equal_to(entry_type));
591 }
592
593 expect!(CatalogueEntryType::from_proto_value(99)).to(be_none());
594 expect!(CatalogueEntryType::from_proto_name("NOT_AN_ENTRY_TYPE")).to(be_none());
595 }
596
597 #[test]
598 fn entry_types_shared_with_v1_keep_their_v1_wire_values() {
599 expect!(CatalogueEntryType::CONTENT_MATCHER.to_proto_value())
602 .to(be_equal_to(EntryType::ContentMatcher as i32));
603 expect!(CatalogueEntryType::CONTENT_GENERATOR.to_proto_value())
604 .to(be_equal_to(EntryType::ContentGenerator as i32));
605 expect!(CatalogueEntryType::TRANSPORT.to_proto_value())
606 .to(be_equal_to(EntryType::Transport as i32));
607 expect!(CatalogueEntryType::MATCHER.to_proto_value())
608 .to(be_equal_to(EntryType::Matcher as i32));
609 expect!(CatalogueEntryType::INTERACTION.to_proto_value())
610 .to(be_equal_to(EntryType::Interaction as i32));
611 }
612
613 #[test]
614 fn registers_a_generator_entry_under_its_own_entry_type() {
615 let name = "registers_a_generator_entry_under_its_own_entry_type";
616 let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
617 let entries = vec![
620 ProtoCatalogueEntry {
621 r#type: CatalogueEntryType::GENERATOR.to_proto_value(),
622 key: name.to_string(),
623 values: hashmap!{}
624 }
625 ];
626
627 register_plugin_entries(&manifest, &entries);
628
629 let entry = lookup_entry(&format!("generator/{}", name));
630 let as_a_content_matcher = lookup_entry(&format!("content-matcher/{}", name));
631 remove_plugin_entries(name);
632
633 expect!(entry.map(|entry| entry.entry_type)).to(be_some().value(CatalogueEntryType::GENERATOR));
634 expect!(as_a_content_matcher).to(be_none());
635 }
636
637 #[test]
638 fn ignores_a_catalogue_entry_whose_type_this_driver_does_not_understand() {
639 let name = "ignores_a_catalogue_entry_whose_type_this_driver_does_not_understand";
640 let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
641 let entries = vec![
642 ProtoCatalogueEntry { r#type: 99, key: name.to_string(), values: hashmap!{} }
643 ];
644
645 register_plugin_entries(&manifest, &entries);
646
647 let registered = all_entries().into_iter().find(|entry| entry.key == name);
648 remove_plugin_entries(name);
649
650 expect!(registered).to(be_none());
651 }
652
653 #[test]
654 fn find_content_matcher_requires_the_whole_base_type_to_match() {
655 let manifest = PactPluginManifest {
656 name: "find_content_matcher_requires_the_whole_base_type_to_match".to_string(),
657 .. PactPluginManifest::default()
658 };
659 let entries = vec![
660 ProtoCatalogueEntry {
661 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
662 key: "jwt".to_string(),
663 values: hashmap!{ "content-types".to_string() => "application/jwt;application/jwt\\+json".to_string() }
665 }
666 ];
667 register_plugin_entries(&manifest, &entries);
668
669 let exact_match = find_content_matcher("application/jwt+json");
670 let with_params = find_content_matcher("application/jwt+json;charset=utf-8");
671 let longer_type = find_content_matcher("application/jwt+jsonextra");
672 let unrelated_type = find_content_matcher("application/json");
673
674 remove_plugin_entries("find_content_matcher_requires_the_whole_base_type_to_match");
675
676 expect!(exact_match).to(be_some());
677 expect!(with_params).to(be_some());
678 expect!(longer_type).to(be_none());
679 expect!(unrelated_type).to(be_none());
680 }
681
682 #[test]
683 fn resolve_capability_resolves_an_unambiguous_core_entry() {
684 let key = "resolve_capability_resolves_an_unambiguous_core_entry";
685 register_core_entries(&vec![CatalogueEntry {
686 entry_type: CatalogueEntryType::CONTENT_MATCHER,
687 provider_type: CatalogueEntryProviderType::CORE,
688 plugin: None,
689 key: key.to_string(),
690 values: hashmap!{}
691 }]);
692
693 let resolved = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER).unwrap();
694
695 let core_key = match resolved {
696 ResolvedCapability::Core(core_key) => core_key,
697 ResolvedCapability::Plugin(_) => panic!("expected a Core resolution, got Plugin")
698 };
699 expect!(core_key).to(be_equal_to(key.to_string()));
700 }
701
702 fn register_core_matcher_entries() {
706 let entries = ["v2-regex", "v2-type", "v3-number-type", "v3-integer-type", "v3-decimal-type",
707 "v3-date", "v3-time", "v3-datetime", "v2-min-type", "v2-max-type", "v2-minmax-type",
708 "v3-includes", "v3-null", "v4-equals-ignore-order", "v4-min-equals-ignore-order",
709 "v4-max-equals-ignore-order", "v4-minmax-equals-ignore-order", "v3-content-type",
710 "v4-array-contains", "v1-equality", "v4-not-empty", "v4-semver"]
711 .iter()
712 .map(|key| CatalogueEntry {
713 entry_type: CatalogueEntryType::MATCHER,
714 provider_type: CatalogueEntryProviderType::CORE,
715 plugin: None,
716 key: key.to_string(),
717 values: hashmap!{}
718 })
719 .collect();
720 register_core_entries(&entries);
721 }
722
723 fn resolved_core_key(entry_key: &str) -> String {
724 match resolve_capability(entry_key, CatalogueEntryType::MATCHER) {
725 Ok(ResolvedCapability::Core(key)) => key,
726 other => panic!("expected '{}' to resolve to a core entry, got {:?}", entry_key, other)
727 }
728 }
729
730 #[test]
731 fn resolve_capability_falls_back_to_the_versioned_core_key() {
732 register_core_matcher_entries();
733
734 expect!(resolved_core_key("type")).to(be_equal_to("v2-type".to_string()));
737 expect!(resolved_core_key("regex")).to(be_equal_to("v2-regex".to_string()));
738 expect!(resolved_core_key("date")).to(be_equal_to("v3-date".to_string()));
739 expect!(resolved_core_key("equality")).to(be_equal_to("v1-equality".to_string()));
740 expect!(resolved_core_key("semver")).to(be_equal_to("v4-semver".to_string()));
741 expect!(resolved_core_key("not-empty")).to(be_equal_to("v4-not-empty".to_string()));
742
743 expect!(resolved_core_key("content-type")).to(be_equal_to("v3-content-type".to_string()));
746 expect!(resolved_core_key("min-type")).to(be_equal_to("v2-min-type".to_string()));
747 expect!(resolved_core_key("equals-ignore-order"))
748 .to(be_equal_to("v4-equals-ignore-order".to_string()));
749
750 expect!(resolved_core_key("v2-type")).to(be_equal_to("v2-type".to_string()));
752 expect!(resolved_core_key("core/matcher/v3-date")).to(be_equal_to("v3-date".to_string()));
753 expect!(resolved_core_key("matcher/v3-date")).to(be_equal_to("v3-date".to_string()));
754 }
755
756 #[test]
757 fn resolve_capability_does_not_match_a_key_component_as_a_substring() {
758 expect!(names_catalogue_key("core/matcher/v2-type", "type")).to(be_false());
762 expect!(names_catalogue_key("core/matcher/v2-type", "v2-type")).to(be_true());
763 expect!(names_catalogue_key("core/matcher/v2-type", "matcher/v2-type")).to(be_true());
764 expect!(names_catalogue_key("core/matcher/v2-type", "core/matcher/v2-type")).to(be_true());
765 expect!(names_catalogue_key("core/matcher/v2-type", "r/v2-type")).to(be_false());
766 expect!(names_catalogue_key("core/content-matcher/xml", "xml")).to(be_true());
767 expect!(names_catalogue_key("core/content-matcher/xml", "ml")).to(be_false());
768 }
769
770 #[test]
771 fn the_versioned_fallback_only_applies_to_matcher_and_generator_entries() {
772 let name = "the_versioned_fallback_only_applies_to_matcher_and_generator_entries";
775 register_core_entries(&vec![
776 CatalogueEntry {
777 entry_type: CatalogueEntryType::CONTENT_MATCHER,
778 provider_type: CatalogueEntryProviderType::CORE,
779 plugin: None,
780 key: format!("v2-{}", name),
781 values: hashmap!{}
782 },
783 CatalogueEntry {
784 entry_type: CatalogueEntryType::MATCHER,
785 provider_type: CatalogueEntryProviderType::CORE,
786 plugin: None,
787 key: format!("v2-matcher-{}", name),
788 values: hashmap!{}
789 }
790 ]);
791
792 expect!(resolve_capability(name, CatalogueEntryType::CONTENT_MATCHER).is_err()).to(be_true());
794 expect!(lookup_entry(name).map(|entry| entry.key)).to(be_none());
795 expect!(resolve_capability(&format!("v2-{}", name), CatalogueEntryType::CONTENT_MATCHER).is_ok())
796 .to(be_true());
797
798 expect!(resolved_core_key(&format!("matcher-{}", name)))
800 .to(be_equal_to(format!("v2-matcher-{}", name)));
801 }
802
803 #[test]
804 fn lookup_entry_matches_by_name_not_by_substring() {
805 let name = "lookup_entry_matches_by_name_not_by_substring";
806 let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
807 register_plugin_entries(&manifest, &vec![
808 ProtoCatalogueEntry {
809 r#type: CatalogueEntryType::CONTENT_MATCHER.to_proto_value(),
810 key: name.to_string(),
811 values: hashmap!{}
812 },
813 ProtoCatalogueEntry {
814 r#type: CatalogueEntryType::MATCHER.to_proto_value(),
815 key: format!("v3-{}", name),
816 values: hashmap!{}
817 }
818 ]);
819
820 let by_name = lookup_entry(name).map(|entry| entry.entry_type);
821 let by_components = lookup_entry(&format!("content-matcher/{}", name)).map(|entry| entry.entry_type);
822 let fully_qualified = lookup_entry(&format!("plugin/{}/content-matcher/{}", name, name))
823 .map(|entry| entry.entry_type);
824 let by_substring = lookup_entry(&name[3..]);
826 let versioned = lookup_entry(&format!("{}-{}", "matcher-fallback", name));
828
829 remove_plugin_entries(name);
830
831 expect!(by_name).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
832 expect!(by_components).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
833 expect!(fully_qualified).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
834 expect!(by_substring).to(be_none());
835 expect!(versioned).to(be_none());
836 }
837
838 #[test]
839 fn lookup_entry_falls_back_to_the_versioned_core_key() {
840 register_core_matcher_entries();
841
842 expect!(lookup_entry("type").map(|entry| entry.key)).to(be_some().value("v2-type".to_string()));
843 expect!(lookup_entry("v3-date").map(|entry| entry.key)).to(be_some().value("v3-date".to_string()));
844 expect!(lookup_entry("matcher/v3-date").map(|entry| entry.key)).to(be_some().value("v3-date".to_string()));
845 }
846
847 #[test]
848 fn resolve_capability_prefers_an_entry_named_directly_over_the_versioned_fallback() {
849 let name = "resolve_capability_prefers_an_entry_named_directly_over_the_versioned_fallback";
850 register_core_entries(&vec![CatalogueEntry {
851 entry_type: CatalogueEntryType::MATCHER,
852 provider_type: CatalogueEntryProviderType::CORE,
853 plugin: None,
854 key: format!("v3-{}", name),
855 values: hashmap!{}
856 }]);
857 let core_first = resolved_core_key(name);
859
860 let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
861 register_plugin_entries(&manifest, &vec![ProtoCatalogueEntry {
862 r#type: CatalogueEntryType::MATCHER.to_proto_value(),
863 key: name.to_string(),
864 values: hashmap!{}
865 }]);
866
867 let resolved = resolve_capability(name, CatalogueEntryType::MATCHER);
868 let still_core = resolved_core_key(&format!("v3-{}", name));
870 remove_plugin_entries(name);
871
872 expect!(core_first).to(be_equal_to(format!("v3-{}", name)));
873 match resolved.expect("expected the plugin's own entry to resolve") {
874 ResolvedCapability::Plugin(resolved_manifest) => expect!(resolved_manifest.name).to(be_equal_to(name.to_string())),
875 ResolvedCapability::Core(key) => panic!("expected the plugin entry to win, got core '{}'", key)
876 };
877 expect!(still_core).to(be_equal_to(format!("v3-{}", name)));
878 }
879
880 #[test]
881 fn resolve_capability_returns_a_clear_error_for_an_unregistered_key() {
882 let result = resolve_capability(
883 "resolve_capability_returns_a_clear_error_for_an_unregistered_key",
884 CatalogueEntryType::CONTENT_MATCHER
885 );
886
887 let err = result.expect_err("expected an error for an unregistered key");
888 expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
889 }
890
891 #[test]
892 fn resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape() {
893 let key = "resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape";
894 register_core_entries(&vec![CatalogueEntry {
895 entry_type: CatalogueEntryType::CONTENT_GENERATOR,
896 provider_type: CatalogueEntryProviderType::CORE,
897 plugin: None,
898 key: key.to_string(),
899 values: hashmap!{}
900 }]);
901
902 let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
903
904 let err = result.expect_err("expected an error when the entry is a generator, not a matcher");
905 expect!(err.to_string().contains("is a CONTENT_GENERATOR, not a CONTENT_MATCHER")).to(be_true());
906 }
907
908 #[test]
909 fn resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry() {
910 let key = "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry";
911 let manifest = PactPluginManifest {
912 name: "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry".to_string(),
913 .. PactPluginManifest::default()
914 };
915 register_core_entries(&vec![CatalogueEntry {
916 entry_type: CatalogueEntryType::CONTENT_MATCHER,
917 provider_type: CatalogueEntryProviderType::CORE,
918 plugin: None,
919 key: key.to_string(),
920 values: hashmap!{}
921 }]);
922 register_plugin_entries(&manifest, &vec![ProtoCatalogueEntry {
923 r#type: catalogue_entry::EntryType::ContentMatcher as i32,
924 key: key.to_string(),
925 values: hashmap!{}
926 }]);
927
928 let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
929
930 remove_plugin_entries(&manifest.name);
931
932 let err = result.expect_err("expected an error for a key matching more than one entry");
933 expect!(err.to_string().contains("Ambiguous catalogue entry key")).to(be_true());
934 }
935}