use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error;
use uuid::Uuid;
pub const GTS_PREFIX: &str = gts_id::GTS_PREFIX;
pub const GTS_URI_PREFIX: &str = "gts://";
static GTS_NS: LazyLock<Uuid> = LazyLock::new(|| Uuid::new_v5(&Uuid::NAMESPACE_URL, b"gts"));
#[derive(Debug, Error)]
pub enum GtsError {
#[error("Invalid GTS segment #{num} @ offset {offset}: '{segment}': {cause}")]
Segment {
num: usize,
offset: usize,
segment: String,
cause: String,
},
#[error("Invalid GTS identifier: {id}: {cause}")]
Id { id: String, cause: String },
#[error("Invalid GTS wildcard pattern: {pattern}: {cause}")]
Wildcard { pattern: String, cause: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct GtsIdSegment {
pub num: usize,
pub offset: usize,
pub segment: String,
pub vendor: String,
pub package: String,
pub namespace: String,
pub type_name: String,
pub ver_major: u32,
pub ver_minor: Option<u32>,
pub is_type: bool,
pub is_wildcard: bool,
pub is_uuid_tail: bool,
}
impl GtsIdSegment {
pub fn new(num: usize, offset: usize, segment: &str) -> Result<Self, GtsError> {
let segment = segment.trim().to_owned();
let mut seg = GtsIdSegment {
num,
offset,
segment: segment.clone(),
vendor: String::new(),
package: String::new(),
namespace: String::new(),
type_name: String::new(),
ver_major: 0,
ver_minor: None,
is_type: false,
is_wildcard: false,
is_uuid_tail: false,
};
seg.parse_segment_id(&segment)?;
Ok(seg)
}
fn parse_segment_id(&mut self, segment: &str) -> Result<(), GtsError> {
let parsed = gts_id::validate_segment(self.num, segment, true).map_err(|cause| {
GtsError::Segment {
num: self.num,
offset: self.offset,
segment: self.segment.clone(),
cause,
}
})?;
self.vendor = parsed.vendor;
self.package = parsed.package;
self.namespace = parsed.namespace;
self.type_name = parsed.type_name;
self.ver_major = parsed.ver_major;
self.ver_minor = parsed.ver_minor;
self.is_type = parsed.is_type;
self.is_wildcard = parsed.is_wildcard;
self.is_uuid_tail = parsed.is_uuid_tail;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsID {
pub id: String,
pub gts_id_segments: Vec<GtsIdSegment>,
}
impl GtsID {
pub fn new(id: &str) -> Result<Self, GtsError> {
let raw = id.trim();
let parsed_segments = gts_id::validate_gts_id(raw, true).map_err(|e| match e {
gts_id::GtsIdError::Id { cause, .. } => GtsError::Id {
id: id.to_owned(),
cause,
},
gts_id::GtsIdError::Segment {
num,
offset,
segment,
cause,
} => GtsError::Segment {
num,
offset,
segment,
cause,
},
})?;
let gts_id_segments: Vec<GtsIdSegment> = parsed_segments
.into_iter()
.enumerate()
.map(|(i, p)| GtsIdSegment {
num: i + 1,
offset: p.offset,
segment: p.raw,
vendor: p.vendor,
package: p.package,
namespace: p.namespace,
type_name: p.type_name,
ver_major: p.ver_major,
ver_minor: p.ver_minor,
is_type: p.is_type,
is_wildcard: p.is_wildcard,
is_uuid_tail: p.is_uuid_tail,
})
.collect();
let has_uuid_tail = gts_id_segments.last().is_some_and(|s| s.is_uuid_tail);
if !has_uuid_tail
&& gts_id_segments.len() == 1
&& !gts_id_segments[0].is_type
&& !gts_id_segments[0].is_wildcard
{
return Err(GtsError::Id {
id: id.to_owned(),
cause: "Single-segment instance IDs are prohibited. Instance IDs must be chained with at least one type segment (e.g., 'type~instance')".to_owned(),
});
}
Ok(GtsID {
id: raw.to_owned(),
gts_id_segments,
})
}
#[must_use]
pub fn is_type(&self) -> bool {
self.id.ends_with('~')
}
#[must_use]
pub fn get_type_id(&self) -> Option<String> {
if self.gts_id_segments.len() < 2 {
return None;
}
let segments: String = self.gts_id_segments[..self.gts_id_segments.len() - 1]
.iter()
.map(|s| s.segment.as_str())
.collect::<Vec<_>>()
.join("");
Some(format!("{GTS_PREFIX}{segments}"))
}
#[must_use]
pub fn to_uuid(&self) -> Uuid {
Uuid::new_v5(>S_NS, self.id.as_bytes())
}
#[must_use]
pub fn is_valid(s: &str) -> bool {
if !s.starts_with(GTS_PREFIX) {
return false;
}
Self::new(s).is_ok()
}
#[must_use]
pub fn wildcard_match(&self, pattern: &GtsWildcard) -> bool {
let p = &pattern.id;
if !p.contains('*') {
return Self::match_segments(&pattern.gts_id_segments, &self.gts_id_segments);
}
if p.matches('*').count() > 1 || !p.ends_with('*') {
return false;
}
Self::match_segments(&pattern.gts_id_segments, &self.gts_id_segments)
}
fn match_segments(pattern_segs: &[GtsIdSegment], candidate_segs: &[GtsIdSegment]) -> bool {
if pattern_segs.len() > candidate_segs.len() {
return false;
}
for (i, p_seg) in pattern_segs.iter().enumerate() {
let c_seg = &candidate_segs[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;
}
if p_seg.is_type && p_seg.is_type != c_seg.is_type {
return false;
}
return true;
}
if p_seg.is_uuid_tail && p_seg.segment != c_seg.segment {
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
}
pub fn split_at_path(gts_with_path: &str) -> Result<(String, Option<String>), GtsError> {
if !gts_with_path.contains('@') {
return Ok((gts_with_path.to_owned(), None));
}
let parts: Vec<&str> = gts_with_path.splitn(2, '@').collect();
let gts = parts[0].to_owned();
let path = parts.get(1).map(|s| (*s).to_owned());
if let Some(ref p) = path
&& p.is_empty()
{
return Err(GtsError::Id {
id: gts_with_path.to_owned(),
cause: "Attribute path cannot be empty".to_owned(),
});
}
Ok((gts, path))
}
}
impl fmt::Display for GtsID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
impl FromStr for GtsID {
type Err = GtsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl AsRef<str> for GtsID {
fn as_ref(&self) -> &str {
&self.id
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GtsWildcard {
pub id: String,
pub gts_id_segments: Vec<GtsIdSegment>,
}
impl GtsWildcard {
fn prefix_str(pattern: &str) -> &str {
match pattern.find('*') {
Some(idx) => &pattern[..idx],
None => pattern,
}
}
#[must_use]
pub fn overlaps(&self, other: &GtsWildcard) -> bool {
let a = Self::prefix_str(&self.id);
let b = Self::prefix_str(&other.id);
a.starts_with(b) || b.starts_with(a)
}
#[must_use]
pub fn is_subset_of(&self, other: &GtsWildcard) -> bool {
let a = Self::prefix_str(&self.id);
let b = Self::prefix_str(&other.id);
a.starts_with(b)
}
pub fn new(pattern: &str) -> Result<Self, GtsError> {
let p = pattern.trim();
if !p.starts_with(GTS_PREFIX) {
return Err(GtsError::Wildcard {
pattern: pattern.to_owned(),
cause: format!("Does not start with '{GTS_PREFIX}'"),
});
}
if p.matches('*').count() > 1 {
return Err(GtsError::Wildcard {
pattern: pattern.to_owned(),
cause: "The wildcard '*' token is allowed only once".to_owned(),
});
}
if p.contains('*') && !p.ends_with(".*") && !p.ends_with("~*") {
return Err(GtsError::Wildcard {
pattern: pattern.to_owned(),
cause: "The wildcard '*' token is allowed only at the end of the pattern"
.to_owned(),
});
}
let gts_id = GtsID::new(p).map_err(|e| GtsError::Wildcard {
pattern: pattern.to_owned(),
cause: e.to_string(),
})?;
Ok(GtsWildcard {
id: gts_id.id,
gts_id_segments: gts_id.gts_id_segments,
})
}
}
impl fmt::Display for GtsWildcard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
impl FromStr for GtsWildcard {
type Err = GtsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl AsRef<str> for GtsWildcard {
fn as_ref(&self) -> &str {
&self.id
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsEntityId(String);
impl GtsEntityId {
#[must_use]
fn new(id: &str) -> Self {
Self(id.to_owned())
}
#[must_use]
fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for GtsEntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsEntityId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<GtsEntityId> for String {
fn from(id: GtsEntityId) -> Self {
id.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsInstanceId(GtsEntityId);
impl serde::Serialize for GtsInstanceId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> serde::Deserialize<'de> for GtsInstanceId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(GtsInstanceId(GtsEntityId(s)))
}
}
impl schemars::JsonSchema for GtsInstanceId {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("GtsInstanceId")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
let json = Self::json_schema_value();
let mut schema_json = serde_json::json!({
"type": "string"
});
if let Some(format) = json.get("format") {
schema_json["format"] = format.clone();
}
if let Some(title) = json.get("title") {
schema_json["title"] = title.clone();
}
if let Some(description) = json.get("description") {
schema_json["description"] = description.clone();
}
if let Some(gts_ref) = json.get("x-gts-ref") {
schema_json["x-gts-ref"] = gts_ref.clone();
}
schema_json.try_into().unwrap_or_default()
}
}
impl GtsInstanceId {
#[must_use]
pub fn json_schema_value() -> serde_json::Value {
serde_json::json!({
"type": "string",
"format": "gts-instance-id",
"title": "GTS Instance ID",
"description": "GTS instance identifier",
"x-gts-ref": "gts.*"
})
}
#[must_use]
pub fn new(schema_id: &str, segment: &str) -> Self {
Self(GtsEntityId::new(&format!("{schema_id}{segment}")))
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl fmt::Display for GtsInstanceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsInstanceId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<GtsInstanceId> for String {
fn from(id: GtsInstanceId) -> Self {
id.0.into()
}
}
impl std::ops::Deref for GtsInstanceId {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl PartialEq<str> for GtsInstanceId {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for GtsInstanceId {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl PartialEq<String> for GtsInstanceId {
fn eq(&self, other: &String) -> bool {
self.0.as_ref() == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsSchemaId(GtsEntityId);
impl serde::Serialize for GtsSchemaId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> serde::Deserialize<'de> for GtsSchemaId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(GtsSchemaId(GtsEntityId(s)))
}
}
impl schemars::JsonSchema for GtsSchemaId {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("GtsSchemaId")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
let json = Self::json_schema_value();
let mut schema_json = serde_json::json!({
"type": "string"
});
if let Some(format) = json.get("format") {
schema_json["format"] = format.clone();
}
if let Some(title) = json.get("title") {
schema_json["title"] = title.clone();
}
if let Some(description) = json.get("description") {
schema_json["description"] = description.clone();
}
if let Some(gts_ref) = json.get("x-gts-ref") {
schema_json["x-gts-ref"] = gts_ref.clone();
}
schema_json.try_into().unwrap_or_default()
}
}
impl GtsSchemaId {
#[must_use]
pub fn json_schema_value() -> serde_json::Value {
serde_json::json!({
"type": "string",
"format": "gts-schema-id",
"title": "GTS Schema ID",
"description": "GTS schema identifier",
"x-gts-ref": "gts.*"
})
}
#[must_use]
pub fn new(schema_id: &str) -> Self {
Self(GtsEntityId::new(schema_id))
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl fmt::Display for GtsSchemaId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsSchemaId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<GtsSchemaId> for String {
fn from(id: GtsSchemaId) -> Self {
id.0.into()
}
}
impl std::ops::Deref for GtsSchemaId {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl PartialEq<str> for GtsSchemaId {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for GtsSchemaId {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl PartialEq<String> for GtsSchemaId {
fn eq(&self, other: &String) -> bool {
self.0.as_ref() == other
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_gts_id_valid() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert_eq!(id.id, "gts.x.core.events.event.v1~");
assert!(id.is_type());
assert_eq!(id.gts_id_segments.len(), 1);
}
#[test]
fn test_gts_id_with_minor_version() {
let id = GtsID::new("gts.x.core.events.event.v1.2~").expect("test");
assert_eq!(id.id, "gts.x.core.events.event.v1.2~");
assert!(id.is_type());
let seg = &id.gts_id_segments[0];
assert_eq!(seg.vendor, "x");
assert_eq!(seg.package, "core");
assert_eq!(seg.namespace, "events");
assert_eq!(seg.type_name, "event");
assert_eq!(seg.ver_major, 1);
assert_eq!(seg.ver_minor, Some(2));
}
#[test]
fn test_gts_id_instance() {
let id = GtsID::new("gts.x.core.events.event.v1~a.b.c.d.v1.0").expect("test");
assert_eq!(id.id, "gts.x.core.events.event.v1~a.b.c.d.v1.0");
assert!(!id.is_type());
}
#[test]
fn test_gts_id_invalid_uppercase() {
let result = GtsID::new("gts.X.core.events.event.v1~");
assert!(result.is_err());
}
#[test]
fn test_gts_id_invalid_no_prefix() {
let result = GtsID::new("x.core.events.event.v1~");
assert!(result.is_err());
}
#[test]
fn test_gts_id_invalid_hyphen() {
let result = GtsID::new("gts.x-vendor.core.events.event.v1~");
assert!(result.is_err());
}
#[test]
fn test_gts_wildcard_simple() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert!(id.wildcard_match(&pattern));
}
#[test]
fn test_gts_wildcard_no_match() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
let id = GtsID::new("gts.y.core.events.event.v1~").expect("test");
assert!(!id.wildcard_match(&pattern));
}
#[test]
fn test_gts_wildcard_type_suffix() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert!(id.wildcard_match(&pattern));
}
#[test]
fn test_uuid_generation() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
let uuid1 = id.to_uuid();
let uuid2 = id.to_uuid();
assert_eq!(uuid1, uuid2);
assert!(!uuid1.to_string().is_empty());
}
#[test]
fn test_uuid_different_ids() {
let id1 = GtsID::new("gts.x.core.events.event.v1~").expect("test");
let id2 = GtsID::new("gts.x.core.events.event.v2~").expect("test");
assert_ne!(id1.to_uuid(), id2.to_uuid());
}
#[test]
fn test_get_type_id() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
let type_id = id.get_type_id();
assert!(type_id.is_none());
let chained =
GtsID::new("gts.x.core.events.type.v1~vendor.app._.custom.v1~").expect("test");
let base_type = chained.get_type_id();
assert!(base_type.is_some());
assert_eq!(base_type.expect("test"), "gts.x.core.events.type.v1~");
}
#[test]
fn test_split_at_path() {
let (gts, path) =
GtsID::split_at_path("gts.x.core.events.event.v1~@field.subfield").expect("test");
assert_eq!(gts, "gts.x.core.events.event.v1~");
assert_eq!(path, Some("field.subfield".to_owned()));
}
#[test]
fn test_split_at_path_no_path() {
let (gts, path) = GtsID::split_at_path("gts.x.core.events.event.v1~").expect("test");
assert_eq!(gts, "gts.x.core.events.event.v1~");
assert_eq!(path, None);
}
#[test]
fn test_split_at_path_empty_path_error() {
let result = GtsID::split_at_path("gts.x.core.events.event.v1~@");
assert!(result.is_err());
}
#[test]
fn test_is_valid() {
assert!(GtsID::is_valid("gts.x.core.events.event.v1~"));
assert!(!GtsID::is_valid("invalid"));
assert!(!GtsID::is_valid("gts.X.core.events.event.v1~"));
}
#[test]
fn test_version_flexibility_in_matching() {
let pattern = GtsWildcard::new("gts.x.core.events.event.v1~").expect("test");
let id_no_minor = GtsID::new("gts.x.core.events.event.v1~").expect("test");
let id_with_minor = GtsID::new("gts.x.core.events.event.v1.0~").expect("test");
assert!(id_no_minor.wildcard_match(&pattern));
assert!(id_with_minor.wildcard_match(&pattern));
}
#[test]
fn test_chained_identifiers() {
let id =
GtsID::new("gts.x.core.events.type.v1~vendor.app._.custom_event.v1~").expect("test");
assert_eq!(id.gts_id_segments.len(), 2);
assert_eq!(id.gts_id_segments[0].vendor, "x");
assert_eq!(id.gts_id_segments[1].vendor, "vendor");
}
#[test]
fn test_gts_id_segment_validation() {
let result = GtsIdSegment::new(0, 0, "invalid-segment");
assert!(result.is_err());
let result = GtsIdSegment::new(0, 0, "x.core.events.event.v1");
assert!(result.is_ok());
}
#[test]
fn test_gts_id_with_underscore() {
let id = GtsID::new("gts.x.core._.event.v1~").expect("test");
assert_eq!(id.gts_id_segments[0].namespace, "_");
}
#[test]
fn test_gts_wildcard_exact_match() {
let pattern = GtsWildcard::new("gts.x.core.events.event.v1~").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert!(id.wildcard_match(&pattern));
}
#[test]
fn test_gts_wildcard_version_mismatch() {
let pattern = GtsWildcard::new("gts.x.core.events.event.v2~").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert!(!id.wildcard_match(&pattern));
}
#[test]
fn test_gts_wildcard_with_minor_version() {
let pattern = GtsWildcard::new("gts.x.core.events.event.v1.0~").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1.0~").expect("test");
assert!(id.wildcard_match(&pattern));
}
#[test]
fn test_gts_wildcard_invalid_pattern() {
let result = GtsWildcard::new("invalid");
assert!(result.is_err());
}
#[test]
fn test_gts_id_invalid_version_format() {
let result = GtsID::new("gts.x.core.events.event.vX~");
assert!(result.is_err());
}
#[test]
fn test_gts_id_missing_segments() {
let result = GtsID::new("gts.x.core~");
assert!(result.is_err());
}
#[test]
fn test_gts_id_empty_segment() {
let result = GtsID::new("gts.x..events.event.v1~");
assert!(result.is_err());
}
#[test]
fn test_gts_wildcard_multiple_wildcards_error() {
let result = GtsWildcard::new("gts.*.*.*.*");
assert!(result.is_err());
}
#[test]
fn test_split_at_path_multiple_at_signs() {
let (gts, path) =
GtsID::split_at_path("gts.x.core.events.event.v1~@field@subfield").expect("test");
assert_eq!(gts, "gts.x.core.events.event.v1~");
assert_eq!(path, Some("field@subfield".to_owned()));
}
#[test]
fn test_gts_wildcard_instance_match() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
let id = GtsID::new("gts.x.core.events.event.v1~a.b.c.d.v1.0").expect("test");
assert!(id.wildcard_match(&pattern));
}
#[test]
fn test_gts_id_whitespace_trimming() {
let id = GtsID::new(" gts.x.core.events.event.v1~ ").expect("test");
assert_eq!(id.id, "gts.x.core.events.event.v1~");
}
#[test]
fn test_gts_wildcard_whitespace_trimming() {
let pattern = GtsWildcard::new(" gts.x.core.events.* ").expect("test");
assert_eq!(pattern.id, "gts.x.core.events.*");
}
#[test]
fn test_gts_id_long_chain() {
let id = GtsID::new("gts.a.b.c.d.v1~e.f.g.h.v2~i.j.k.l.v3~").expect("test");
assert_eq!(id.gts_id_segments.len(), 3);
}
#[test]
fn test_gts_wildcard_only_at_end() {
let result1 = GtsWildcard::new("gts.*.core.events.event.v1~");
assert!(result1.is_err());
let pattern2 = GtsWildcard::new("gts.x.core.events.*").expect("test");
let id2 = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert!(id2.wildcard_match(&pattern2));
}
#[test]
fn test_gts_id_version_without_minor() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert_eq!(id.gts_id_segments[0].ver_major, 1);
assert_eq!(id.gts_id_segments[0].ver_minor, None);
}
#[test]
fn test_gts_id_version_with_large_numbers() {
let id = GtsID::new("gts.x.core.events.event.v99.999~").expect("test");
assert_eq!(id.gts_id_segments[0].ver_major, 99);
assert_eq!(id.gts_id_segments[0].ver_minor, Some(999));
}
#[test]
fn test_gts_wildcard_no_wildcard_different_vendor() {
let pattern = GtsWildcard::new("gts.x.core.events.event.v1~").expect("test");
let id = GtsID::new("gts.y.core.events.event.v1~").expect("test");
assert!(!id.wildcard_match(&pattern));
}
#[test]
fn test_gts_id_invalid_double_tilde() {
let result = GtsID::new("gts.x.core.events.event.v1~~");
assert!(result.is_err());
}
#[test]
fn test_split_at_path_with_hash() {
let (gts, path) = GtsID::split_at_path("gts.x.core.events.event.v1~#field").expect("test");
assert_eq!(gts, "gts.x.core.events.event.v1~#field");
assert_eq!(path, None);
}
#[test]
fn test_gts_id_display_trait() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
assert_eq!(format!("{id}"), "gts.x.core.events.event.v1~");
}
#[test]
fn test_gts_id_from_str_trait() {
let id: GtsID = "gts.x.core.events.event.v1~".parse().expect("test");
assert_eq!(id.id, "gts.x.core.events.event.v1~");
}
#[test]
fn test_gts_id_as_ref_trait() {
let id = GtsID::new("gts.x.core.events.event.v1~").expect("test");
let s: &str = id.as_ref();
assert_eq!(s, "gts.x.core.events.event.v1~");
}
#[test]
fn test_gts_wildcard_display_trait() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
assert_eq!(format!("{pattern}"), "gts.x.core.events.*");
}
#[test]
fn test_gts_wildcard_from_str_trait() {
let pattern: GtsWildcard = "gts.x.core.events.*".parse().expect("test");
assert_eq!(pattern.id, "gts.x.core.events.*");
}
#[test]
fn test_gts_wildcard_as_ref_trait() {
let pattern = GtsWildcard::new("gts.x.core.events.*").expect("test");
let s: &str = pattern.as_ref();
assert_eq!(s, "gts.x.core.events.*");
}
#[test]
fn test_gts_id_new_with_uri_prefix() {
assert!(GtsID::new("gts://x.core.v1~").is_err());
}
#[test]
fn test_gts_id_minimum_segments() {
assert!(GtsID::new("gts~").is_err());
assert!(GtsID::new("gts.x~").is_err());
assert!(GtsID::new("gts.x.pkg~").is_err());
assert!(GtsID::new("gts.x.pkg.ns~").is_err());
assert!(GtsID::new("gts.x.pkg.ns.type.v1~").is_ok());
}
#[test]
fn test_gts_id_invalid_characters() {
assert!(GtsID::new("gts.x.test!.v1~").is_err());
assert!(GtsID::new("gts.x.te$t.v1~").is_err());
assert!(GtsID::new("gts.x.te st.v1~").is_err());
}
#[test]
fn test_gts_id_uppercase_rejected() {
assert!(GtsID::new("gts.x.Test.v1~").is_err());
assert!(GtsID::new("gts.X.test.v1~").is_err());
}
#[test]
fn test_gts_id_hyphen_rejected() {
assert!(GtsID::new("gts.x.test-name.v1~").is_err());
}
#[test]
fn test_gts_id_digit_start_segment() {
assert!(GtsID::new("gts.x.9test.v1~").is_err());
}
#[test]
fn test_gts_id_with_numbers_midword() {
assert!(GtsID::new("gts.x.test2name.ns.type.v1~").is_ok());
assert!(GtsID::new("gts.x.pkg.item3.type.v1~").is_ok());
}
#[test]
fn test_gts_wildcard_type_suffix_match() {
let pattern = GtsWildcard::new("gts.x.pkg.ns.type.v1~*").expect("test");
let id1 = GtsID::new("gts.x.pkg.ns.type.v1~a.b.c.child.v1~").expect("test");
let id2 = GtsID::new("gts.x.pkg.ns.type.v2~a.b.c.child.v1~").expect("test");
assert!(id1.wildcard_match(&pattern));
assert!(!id2.wildcard_match(&pattern));
}
#[test]
fn test_split_at_path_valid_json_pointer() {
let (gts, path) = GtsID::split_at_path("gts.x.test.v1~@/properties/field").expect("test");
assert_eq!(gts, "gts.x.test.v1~");
assert_eq!(path, Some("/properties/field".to_owned()));
}
#[test]
fn test_gts_id_segment_start_underscore() {
assert!(GtsID::new("gts.x._private.event.v1~").is_err());
}
#[test]
fn test_gts_id_multi_digit_versions() {
assert!(GtsID::new("gts.x.pkg.ns.event.v10~").is_ok());
assert!(GtsID::new("gts.x.pkg.ns.event.v1.20~").is_ok());
}
#[test]
fn test_gts_segment_too_many_tildes() {
let seg = GtsIdSegment::new(1, 0, "x.pkg.ns.type.v1~~");
assert!(seg.is_err());
if let Err(e) = seg {
assert!(e.to_string().contains("Too many '~' characters"));
}
}
#[test]
fn test_gts_segment_tilde_not_at_end() {
let seg = GtsIdSegment::new(1, 0, "x.pkg~mid.ns.type.v1");
assert!(seg.is_err());
if let Err(e) = seg {
assert!(e.to_string().contains("'~' must be at the end"));
}
}
#[test]
fn test_gts_segment_too_many_tokens() {
let seg = GtsIdSegment::new(1, 0, "x.pkg.ns.type.v1.2.extra~");
assert!(seg.is_err());
if let Err(e) = seg {
assert!(e.to_string().contains("Too many tokens"));
}
}
#[test]
fn test_gts_segment_version_without_v_prefix() {
let seg = GtsIdSegment::new(1, 0, "x.pkg.ns.type.1~");
assert!(seg.is_err());
if let Err(e) = seg {
assert!(e.to_string().contains("Major version must start with 'v'"));
}
}
#[test]
fn test_gts_segment_version_leading_zeros() {
let seg = GtsIdSegment::new(1, 0, "x.pkg.ns.type.v01~");
assert!(seg.is_err());
if let Err(e) = seg {
assert!(e.to_string().contains("Major version must be an integer"));
}
}
#[test]
fn test_gts_wildcard_at_various_positions() {
let result = GtsWildcard::new("gts.*");
assert!(result.is_ok());
let result = GtsWildcard::new("gts.x.*");
assert!(result.is_ok());
let result = GtsWildcard::new("gts.x.pkg.*");
assert!(result.is_ok());
let result = GtsWildcard::new("gts.x.pkg.ns.*");
assert!(result.is_ok());
let result = GtsWildcard::new("gts.x.pkg.ns.type.*");
assert!(result.is_ok());
}
#[test]
fn test_overlaps_broad_and_narrow() {
let broad = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let narrow = GtsWildcard::new("gts.x.core.srr.resource.v1~acme.*").expect("test");
assert!(broad.overlaps(&narrow));
assert!(narrow.overlaps(&broad)); }
#[test]
fn test_overlaps_disjoint_types() {
let a = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let b = GtsWildcard::new("gts.x.core.other.resource.v1~*").expect("test");
assert!(!a.overlaps(&b));
assert!(!b.overlaps(&a));
}
#[test]
fn test_overlaps_same_pattern() {
let a = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let b = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
assert!(a.overlaps(&b));
}
#[test]
fn test_overlaps_exact_vs_wildcard() {
let exact =
GtsWildcard::new("gts.x.core.srr.resource.v1~acme.crm._.contact.v1~").expect("test");
let broad = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
assert!(exact.overlaps(&broad));
assert!(broad.overlaps(&exact));
}
#[test]
fn test_overlaps_tilde_star_chain() {
let base = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let sub = GtsWildcard::new("gts.x.core.srr.resource.v1~acme.crm.*").expect("test");
assert!(base.overlaps(&sub));
}
#[test]
fn test_subset_narrow_is_subset_of_broad() {
let broad = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let narrow = GtsWildcard::new("gts.x.core.srr.resource.v1~acme.*").expect("test");
assert!(narrow.is_subset_of(&broad));
assert!(!broad.is_subset_of(&narrow));
}
#[test]
fn test_subset_identical_patterns() {
let a = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let b = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
assert!(a.is_subset_of(&b)); assert!(b.is_subset_of(&a));
}
#[test]
fn test_subset_disjoint_not_subset() {
let a = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let b = GtsWildcard::new("gts.x.core.other.resource.v1~*").expect("test");
assert!(!a.is_subset_of(&b));
assert!(!b.is_subset_of(&a));
}
#[test]
fn test_subset_exact_is_subset_of_wildcard() {
let exact =
GtsWildcard::new("gts.x.core.srr.resource.v1~acme.crm._.contact.v1~").expect("test");
let broad = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
assert!(exact.is_subset_of(&broad));
assert!(!broad.is_subset_of(&exact));
}
#[test]
fn test_subset_three_levels() {
let l1 = GtsWildcard::new("gts.x.core.srr.resource.v1~*").expect("test");
let l2 = GtsWildcard::new("gts.x.core.srr.resource.v1~acme.*").expect("test");
let l3 = GtsWildcard::new("gts.x.core.srr.resource.v1~acme.crm.*").expect("test");
assert!(l3.is_subset_of(&l2));
assert!(l3.is_subset_of(&l1));
assert!(l2.is_subset_of(&l1));
assert!(!l1.is_subset_of(&l2));
assert!(!l1.is_subset_of(&l3));
assert!(!l2.is_subset_of(&l3));
}
}