use std::fmt;
use std::str::FromStr;
use crate::gts_id_segment::SegmentView;
use crate::{GtsId, GtsIdError, GtsIdPatternSegment};
#[derive(Debug, Clone, PartialEq)]
pub struct GtsIdPattern {
pattern: String,
segments: Vec<GtsIdPatternSegment>,
}
impl GtsIdPattern {
pub fn try_new(pattern: &str) -> Result<Self, GtsIdError> {
let p = pattern.trim();
let segments = crate::parse::parse_pattern(p)?;
Ok(GtsIdPattern {
pattern: p.to_owned(),
segments,
})
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
#[must_use]
pub fn segments(&self) -> &[GtsIdPatternSegment] {
&self.segments
}
#[must_use]
pub fn into_segments(self) -> Vec<GtsIdPatternSegment> {
self.segments
}
#[must_use]
pub fn matches_pattern(&self, pattern: &GtsIdPattern) -> bool {
pattern.matches_views(self.segments())
}
pub(crate) fn matches_views<C: SegmentView>(&self, candidate: &[C]) -> bool {
let pattern_segs = &self.segments;
if pattern_segs.len() > candidate.len() {
return false;
}
for (i, p_seg) in pattern_segs.iter().enumerate() {
let c_seg = &candidate[i];
if p_seg.is_wildcard() {
if !p_seg.vendor().is_empty() && p_seg.vendor() != c_seg.vendor() {
return false;
}
if !p_seg.package().is_empty() && p_seg.package() != c_seg.package() {
return false;
}
if !p_seg.namespace().is_empty() && p_seg.namespace() != c_seg.namespace() {
return false;
}
if !p_seg.type_name().is_empty() && p_seg.type_name() != c_seg.type_name() {
return false;
}
if p_seg.ver_major() != 0 && p_seg.ver_major() != c_seg.ver_major() {
return false;
}
if let Some(p_minor) = p_seg.ver_minor()
&& Some(p_minor) != c_seg.ver_minor()
{
return false;
}
return true;
}
if p_seg.uuid_tail().is_some() && p_seg.uuid_tail() != c_seg.uuid_tail() {
return false;
}
if p_seg.vendor() != c_seg.vendor() {
return false;
}
if p_seg.package() != c_seg.package() {
return false;
}
if p_seg.namespace() != c_seg.namespace() {
return false;
}
if p_seg.type_name() != c_seg.type_name() {
return false;
}
if p_seg.ver_major() != c_seg.ver_major() {
return false;
}
if let Some(p_minor) = p_seg.ver_minor()
&& Some(p_minor) != c_seg.ver_minor()
{
return false;
}
if p_seg.is_type() != c_seg.is_type() {
return false;
}
}
true
}
#[must_use]
pub fn covers(&self, other: &GtsIdPattern) -> bool {
self.matches_views(other.segments())
}
#[must_use]
pub fn is_valid(s: &str) -> bool {
Self::try_new(s).is_ok()
}
}
impl From<&GtsId> for GtsIdPattern {
fn from(id: &GtsId) -> Self {
GtsIdPattern {
pattern: id.id().to_owned(),
segments: id
.segments()
.iter()
.cloned()
.map(GtsIdPatternSegment::Segment)
.collect(),
}
}
}
impl From<GtsId> for GtsIdPattern {
fn from(id: GtsId) -> Self {
let pattern = id.id().to_owned();
GtsIdPattern {
pattern,
segments: id
.into_segments()
.into_iter()
.map(GtsIdPatternSegment::Segment)
.collect(),
}
}
}
impl fmt::Display for GtsIdPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.pattern)
}
}
impl FromStr for GtsIdPattern {
type Err = GtsIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_new(s)
}
}
impl AsRef<str> for GtsIdPattern {
fn as_ref(&self) -> &str {
&self.pattern
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn gts_id(suffix: &str) -> String {
format!("{}{suffix}", crate::GTS_ID_PREFIX)
}
#[test]
fn test_gts_wildcard_simple() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_no_match() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let id = GtsId::try_new(>s_id("y.core.events.event.v1~")).expect("test");
assert!(!id.matches_pattern(&pattern));
}
#[test]
fn test_trailing_wildcard_ignores_type_marker() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.topic.v1~*")).expect("test");
let type_candidate = GtsId::try_new(>s_id(
"x.core.events.topic.v1~vendor.app.orders.thing.v1~",
))
.expect("test");
let instance_candidate = GtsId::try_new(>s_id(
"x.core.events.topic.v1~vendor.app.orders.thing.v1.0",
))
.expect("test");
assert!(type_candidate.matches_pattern(&pattern));
assert!(instance_candidate.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_type_suffix() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_version_flexibility_in_matching() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let id_no_minor = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let id_with_minor = GtsId::try_new(>s_id("x.core.events.event.v1.0~")).expect("test");
assert!(id_no_minor.matches_pattern(&pattern));
assert!(id_with_minor.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_exact_match() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_version_mismatch() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.event.v2~")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert!(!id.matches_pattern(&pattern));
}
#[test]
fn test_pattern_longer_than_candidate_does_not_match() {
let pattern = GtsIdPattern::try_new(>s_id(
"x.core.events.topic.v1~vendor.app.orders.order.v1~",
))
.expect("test");
let id = GtsId::try_new(>s_id("x.core.events.topic.v1~")).expect("test");
assert!(!id.matches_pattern(&pattern));
}
#[test]
fn test_uuid_tail_mismatch_does_not_match() {
let pattern = GtsIdPattern::try_new(>s_id(
"x.core.events.topic.v1~7a1d2f34-5678-49ab-9012-abcdef123456",
))
.expect("test");
let id = GtsId::try_new(>s_id(
"x.core.events.topic.v1~7a1d2f34-5678-49ab-9012-abcdef123457",
))
.expect("test");
assert!(!id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_with_minor_version() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.event.v1.0~")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1.0~")).expect("test");
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_invalid_pattern() {
let result = GtsIdPattern::try_new("invalid");
assert!(result.is_err());
}
#[test]
fn test_gts_wildcard_multiple_wildcards_error() {
let result = GtsIdPattern::try_new(>s_id("*.*.*.*"));
assert!(result.is_err());
}
#[test]
fn test_gts_wildcard_instance_match() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let id = GtsId::try_new(>s_id("x.core.events.event.v1~a.b.c.d.v1.0")).expect("test");
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_whitespace_trimming() {
let pattern =
GtsIdPattern::try_new(&format!(" {} ", gts_id("x.core.events.*"))).expect("test");
assert_eq!(pattern.pattern(), gts_id("x.core.events.*"));
}
#[test]
fn test_gts_wildcard_only_at_end() {
let result1 = GtsIdPattern::try_new(>s_id("*.core.events.event.v1~"));
assert!(result1.is_err());
let pattern2 = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let id2 = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert!(id2.matches_pattern(&pattern2));
}
#[test]
fn test_gts_wildcard_no_wildcard_different_vendor() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let id = GtsId::try_new(>s_id("y.core.events.event.v1~")).expect("test");
assert!(!id.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_display_trait() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
assert_eq!(format!("{pattern}"), gts_id("x.core.events.*"));
}
#[test]
fn test_gts_wildcard_from_str_trait() {
let pattern: GtsIdPattern = gts_id("x.core.events.*").parse().expect("test");
assert_eq!(pattern.pattern(), gts_id("x.core.events.*"));
}
#[test]
fn test_gts_wildcard_as_ref_trait() {
let pattern = GtsIdPattern::try_new(>s_id("x.core.events.*")).expect("test");
let s: &str = pattern.as_ref();
assert_eq!(s, gts_id("x.core.events.*"));
}
#[test]
fn test_gts_wildcard_type_suffix_match() {
let pattern = GtsIdPattern::try_new(>s_id("x.pkg.ns.type.v1~*")).expect("test");
let id1 = GtsId::try_new(>s_id("x.pkg.ns.type.v1~a.b.c.child.v1~")).expect("test");
let id2 = GtsId::try_new(>s_id("x.pkg.ns.type.v2~a.b.c.child.v1~")).expect("test");
assert!(id1.matches_pattern(&pattern));
assert!(!id2.matches_pattern(&pattern));
}
#[test]
fn test_gts_wildcard_at_various_positions() {
let result = GtsIdPattern::try_new(>s_id("*"));
assert!(result.is_ok());
let result = GtsIdPattern::try_new(>s_id("x.*"));
assert!(result.is_ok());
let result = GtsIdPattern::try_new(>s_id("x.pkg.*"));
assert!(result.is_ok());
let result = GtsIdPattern::try_new(>s_id("x.pkg.ns.*"));
assert!(result.is_ok());
let result = GtsIdPattern::try_new(>s_id("x.pkg.ns.type.*"));
assert!(result.is_ok());
}
#[test]
fn test_covers_broad_covers_narrow() {
let broad = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
let narrow = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~acme.*")).expect("test");
assert!(broad.covers(&narrow));
assert!(!narrow.covers(&broad));
}
#[test]
fn test_covers_disjoint_types() {
let a = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
let b = GtsIdPattern::try_new(>s_id("x.core.other.resource.v1~*")).expect("test");
assert!(!a.covers(&b));
assert!(!b.covers(&a));
}
#[test]
fn test_covers_identical_patterns() {
let a = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
let b = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
assert!(a.covers(&b));
assert!(b.covers(&a));
}
#[test]
fn test_covers_wildcard_covers_exact() {
let exact = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~acme.crm._.contact.v1~"))
.expect("test");
let broad = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
assert!(broad.covers(&exact));
assert!(!exact.covers(&broad));
}
#[test]
fn test_covers_three_levels() {
let l1 = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~*")).expect("test");
let l2 = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~acme.*")).expect("test");
let l3 = GtsIdPattern::try_new(>s_id("x.core.srr.resource.v1~acme.crm.*")).expect("test");
assert!(l1.covers(&l2));
assert!(l1.covers(&l3));
assert!(l2.covers(&l3));
assert!(!l2.covers(&l1));
assert!(!l3.covers(&l1));
assert!(!l3.covers(&l2));
}
#[test]
fn test_version_wildcard_valid_and_is_wildcard() {
let pattern = GtsIdPattern::try_new(>s_id("x.llm.chat.message.v*")).expect("test");
assert_eq!(pattern.segments().len(), 1);
assert!(pattern.segments()[0].is_wildcard());
assert!(GtsIdPattern::is_valid(>s_id("x.llm.chat.message.v*")));
}
#[test]
fn test_version_wildcard_matches_any_version_and_chain() {
let pattern = GtsIdPattern::try_new(>s_id("x.llm.chat.message.v*")).expect("test");
for id in [
gts_id("x.llm.chat.message.v1.0~"),
gts_id("x.llm.chat.message.v1.1~"),
gts_id("x.llm.chat.message.v2~"),
gts_id("x.llm.chat.message.v1.0~acme.app.ns.derived.v1~"),
] {
let candidate = GtsId::try_new(&id).expect("test");
assert!(candidate.matches_pattern(&pattern), "should match: {id}");
}
let other = GtsId::try_new(>s_id("x.llm.chat.other.v1~")).expect("test");
assert!(!other.matches_pattern(&pattern));
}
#[test]
fn test_version_wildcard_equivalent_to_version_position_star() {
let v_star = GtsIdPattern::try_new(>s_id("x.llm.chat.message.v*")).expect("test");
let dot_star = GtsIdPattern::try_new(>s_id("x.llm.chat.message.*")).expect("test");
for id in [
gts_id("x.llm.chat.message.v1~"),
gts_id("x.llm.chat.message.v9.9~"),
gts_id("x.llm.chat.message.v1.0~acme.app.ns.derived.v1~"),
] {
let candidate = GtsId::try_new(&id).expect("test");
assert_eq!(
candidate.matches_pattern(&v_star),
candidate.matches_pattern(&dot_star),
"match divergence for {id}"
);
}
}
#[test]
fn test_version_wildcard_rejections() {
assert!(!GtsIdPattern::is_valid(>s_id("x.llm.chat.message.v*~*")));
assert!(!GtsIdPattern::is_valid(>s_id("x.llm.chat.message.v1.*~")));
assert!(!GtsIdPattern::is_valid(>s_id("x.llm.chat.msg*")));
}
#[test]
fn test_covers_any_minor_covers_specific_minor() {
let any_minor = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~*")).expect("test");
let specific = GtsIdPattern::try_new(>s_id("x.core.events.event.v1.0~*")).expect("test");
assert!(any_minor.covers(&specific));
assert!(!specific.covers(&any_minor));
}
#[test]
fn test_covers_bare_type_minor_flexibility() {
let any_minor = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let specific = GtsIdPattern::try_new(>s_id("x.core.events.event.v1.0~")).expect("test");
assert!(any_minor.covers(&specific));
assert!(!specific.covers(&any_minor));
}
#[test]
fn test_covers_major_version_mismatch() {
let v1 = GtsIdPattern::try_new(>s_id("x.core.events.event.v1~*")).expect("test");
let v2 = GtsIdPattern::try_new(>s_id("x.core.events.event.v2~*")).expect("test");
assert!(!v1.covers(&v2));
assert!(!v2.covers(&v1));
}
#[test]
fn test_from_gts_id_ref() {
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let pattern = GtsIdPattern::from(&id);
assert_eq!(pattern.pattern(), id.id());
assert!(pattern.segments().iter().all(|s| !s.is_wildcard()));
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_concrete_pattern_covers_derived_chain() {
let pattern = GtsId::try_new(>s_id("a.b.c.d.v1~"))
.expect("test")
.to_pattern();
let exact = GtsId::try_new(>s_id("a.b.c.d.v1~")).expect("test");
let derived = GtsId::try_new(>s_id("a.b.c.d.v1~w.x.y.z.v1")).expect("test");
assert!(exact.matches_pattern(&pattern));
assert!(derived.matches_pattern(&pattern));
let other_base = GtsId::try_new(>s_id("a.b.c.other.v1~w.x.y.z.v1")).expect("test");
assert!(!other_base.matches_pattern(&pattern));
}
#[test]
fn test_from_gts_id_owned() {
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let expected = id.id().to_owned();
let pattern: GtsIdPattern = id.into();
assert_eq!(pattern.pattern(), expected);
assert!(pattern.segments().iter().all(|s| !s.is_wildcard()));
}
#[test]
fn test_from_gts_id_chained_preserves_segments() {
let id = GtsId::try_new(>s_id(
"x.core.events.topic.v1~vendor.app.orders.thing.v1.0",
))
.expect("test");
let pattern = GtsIdPattern::from(&id);
assert_eq!(pattern.segments().len(), id.segments().len());
assert_eq!(pattern.pattern(), id.id());
assert!(id.matches_pattern(&pattern));
}
#[test]
fn test_from_ref_and_owned_agree() {
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
let from_ref = GtsIdPattern::from(&id);
let from_owned = GtsIdPattern::from(id);
assert_eq!(from_ref, from_owned);
}
#[test]
fn test_from_gts_id_matches_to_pattern() {
let id = GtsId::try_new(>s_id("x.core.events.event.v1~")).expect("test");
assert_eq!(GtsIdPattern::from(&id), id.to_pattern());
}
#[test]
fn test_is_valid() {
assert!(GtsIdPattern::is_valid(>s_id("x.core.events.event.v1~")));
assert!(GtsIdPattern::is_valid(>s_id("x.core.events.*")));
assert!(GtsIdPattern::is_valid(>s_id("x.core.events.topic.v1~*")));
assert!(!GtsIdPattern::is_valid("not-a-gts-id"));
assert!(!GtsIdPattern::is_valid(>s_id("x.*.events.event.v1~")));
assert!(!GtsIdPattern::is_valid(>s_id(
"x.core.events.topic.v1.*~"
)));
}
}