use crate::{GTS_ID_PREFIX, GtsIdError, GtsIdPatternSegment, GtsIdSegment};
pub const GTS_ID_MAX_LENGTH: usize = 1024;
#[must_use]
pub fn expected_format(segment_num: usize) -> String {
if segment_num == 1 {
format!("{GTS_ID_PREFIX}vendor.package.namespace.type.vMAJOR[.MINOR]")
} else {
"vendor.package.namespace.type.vMAJOR[.MINOR]".to_owned()
}
}
#[inline]
#[must_use]
pub fn is_uuid(s: &str) -> bool {
s.len() == 36
&& s.char_indices().all(|(i, c)| match i {
8 | 13 | 18 | 23 => c == '-',
_ => c.is_ascii_hexdigit(),
})
}
#[inline]
#[must_use]
pub fn is_valid_segment_token(token: &str) -> bool {
if token.is_empty() {
return false;
}
let mut chars = token.chars();
match chars.next() {
Some(c) if c.is_ascii_lowercase() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
#[inline]
#[must_use]
pub fn parse_u32_exact(value: &str) -> Option<u32> {
let parsed = value.parse::<u32>().ok()?;
if parsed.to_string() == value {
Some(parsed)
} else {
None
}
}
fn split_raw_segments(
id: &str,
allow_wildcards: bool,
) -> Result<(Vec<String>, Option<String>), GtsIdError> {
if !id.starts_with(GTS_ID_PREFIX) {
return Err(GtsIdError::new(
id,
format!("must start with '{GTS_ID_PREFIX}'"),
));
}
if id != id.to_lowercase() {
return Err(GtsIdError::new(id, "must be lowercase"));
}
if id.len() > GTS_ID_MAX_LENGTH {
return Err(GtsIdError::new(
id,
format!("too long ({} chars, max {GTS_ID_MAX_LENGTH})", id.len()),
));
}
if allow_wildcards {
let wildcards_num = id.matches('*').count();
if wildcards_num > 1 {
return Err(GtsIdError::new(
id,
"The wildcard '*' token is allowed only once",
));
}
if wildcards_num > 0 && !id.ends_with('*') {
return Err(GtsIdError::new(
id,
"The wildcard '*' token is allowed only at the end of the pattern",
));
}
}
let remainder = &id[GTS_ID_PREFIX.len()..];
let tilde_parts: Vec<&str> = remainder.split('~').collect();
let uuid_tail: Option<&str> = {
let last = tilde_parts.last().copied().unwrap_or("");
if is_uuid(last) && tilde_parts.len() >= 2 {
Some(last)
} else {
None
}
};
let segments_portion = match uuid_tail {
Some(uuid) => &id[..id.len() - uuid.len() - 1], None => id,
};
if segments_portion.contains('-') {
return Err(GtsIdError::new(id, "must not contain '-'"));
}
let seg_count = tilde_parts.len() - usize::from(uuid_tail.is_some());
let mut segments_raw: Vec<String> = Vec::new();
for (i, &part) in tilde_parts.iter().enumerate().take(seg_count) {
let is_last = i == seg_count - 1;
if part.is_empty() {
if !(is_last && uuid_tail.is_none()) {
return Err(GtsIdError::new(
id,
format!("empty segment at tilde-part #{}", i + 1),
));
}
} else if is_last && uuid_tail.is_none() {
segments_raw.push(part.to_owned());
} else {
segments_raw.push(format!("{part}~"));
}
}
if segments_raw.is_empty() {
return Err(GtsIdError::new(id, "no segments found"));
}
Ok((segments_raw, uuid_tail.map(str::to_owned)))
}
fn single_segment_instance_error(id: &str) -> GtsIdError {
GtsIdError::new(
id,
"Single-segment instance IDs are prohibited. Instance IDs must be chained with at least one type segment (e.g., 'type~instance')",
)
}
pub fn parse_id(id: &str) -> Result<Vec<GtsIdSegment>, GtsIdError> {
let (segments_raw, uuid_tail) = split_raw_segments(id, false)?;
let mut segments = Vec::new();
let mut offset = GTS_ID_PREFIX.len();
for (i, seg) in segments_raw.iter().enumerate() {
let parsed = GtsIdSegment::parse(i + 1, seg)
.map_err(|cause| GtsIdError::new(id, cause).with_segment(i + 1, offset, seg.clone()))?;
offset += seg.len();
segments.push(parsed);
}
if let Some(ref uuid) = uuid_tail {
segments.push(GtsIdSegment::uuid_tail_segment(uuid));
}
if uuid_tail.is_none() && segments.len() == 1 && !segments[0].is_type() {
return Err(single_segment_instance_error(id));
}
Ok(segments)
}
pub fn parse_pattern(id: &str) -> Result<Vec<GtsIdPatternSegment>, GtsIdError> {
let (segments_raw, uuid_tail) = split_raw_segments(id, true)?;
let mut segments = Vec::new();
let mut offset = GTS_ID_PREFIX.len();
for (i, seg) in segments_raw.iter().enumerate() {
let parsed = GtsIdPatternSegment::parse(i + 1, seg)
.map_err(|cause| GtsIdError::new(id, cause).with_segment(i + 1, offset, seg.clone()))?;
offset += seg.len();
segments.push(parsed);
}
if let Some(ref uuid) = uuid_tail {
segments.push(GtsIdPatternSegment::uuid_tail_segment(uuid));
}
if uuid_tail.is_none()
&& segments.len() == 1
&& !segments[0].is_type()
&& !segments[0].is_wildcard()
{
return Err(single_segment_instance_error(id));
}
Ok(segments)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn gts_id(suffix: &str) -> String {
format!("{GTS_ID_PREFIX}{suffix}")
}
#[test]
fn test_valid_tokens() {
assert!(is_valid_segment_token("abc"));
assert!(is_valid_segment_token("a1b2"));
assert!(is_valid_segment_token("_private"));
assert!(is_valid_segment_token("a_b_c"));
}
#[test]
fn test_invalid_tokens() {
assert!(!is_valid_segment_token(""));
assert!(!is_valid_segment_token("1abc"));
assert!(!is_valid_segment_token("ABC"));
assert!(!is_valid_segment_token("a-b"));
assert!(!is_valid_segment_token("a.b"));
}
#[test]
fn test_parse_u32_exact_valid() {
assert_eq!(parse_u32_exact("0"), Some(0));
assert_eq!(parse_u32_exact("1"), Some(1));
assert_eq!(parse_u32_exact("42"), Some(42));
}
#[test]
fn test_parse_u32_exact_rejects_leading_zeros() {
assert_eq!(parse_u32_exact("01"), None);
assert_eq!(parse_u32_exact("007"), None);
}
#[test]
fn test_parse_u32_exact_rejects_non_numeric() {
assert_eq!(parse_u32_exact("abc"), None);
assert_eq!(parse_u32_exact(""), None);
}
#[test]
fn test_valid_gts_id() {
let segments = parse_id(>s_id("x.core.events.event.v1~")).unwrap();
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].vendor(), "x");
assert!(segments[0].is_type());
}
#[test]
fn test_valid_gts_id_chained() {
let segments = parse_id(>s_id(
"x.core.events.type.v1~vendor.app._.custom_event.v1~",
))
.unwrap();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].vendor(), "x");
assert_eq!(segments[1].vendor(), "vendor");
}
#[test]
fn test_gts_id_missing_prefix() {
let err = parse_id("x.core.events.event.v1~").unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(
err.cause
.contains(&format!("must start with '{GTS_ID_PREFIX}'")),
"got: {err}"
);
}
#[test]
fn test_gts_id_uppercase() {
let err = parse_id(>s_id("X.core.events.event.v1~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("lowercase"), "got: {err}");
}
#[test]
fn test_gts_id_hyphen() {
let err = parse_id(>s_id("x-vendor.core.events.event.v1~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("'-'"), "got: {err}");
}
#[test]
fn test_gts_id_segment_error_carries_num_and_offset() {
let err = parse_id(>s_id(
"x.core.modkit.plugin.v1~x.core.license_enforcer.integration.plugin.v1~",
))
.unwrap_err();
let seg = err.segment.as_ref().expect("expected segment-level error");
assert_eq!(seg.num, 2);
let expected_offset = GTS_ID_PREFIX.len() + "x.core.modkit.plugin.v1~".len();
assert_eq!(seg.offset, expected_offset);
assert!(
err.cause.contains("Too many name tokens before version"),
"got: {err}"
);
}
#[test]
fn test_gts_id_instance_no_tilde_end() {
let segments = parse_id(>s_id("x.core.events.event.v1~a.b.c.d.v1.0")).unwrap();
assert_eq!(segments.len(), 2);
assert!(segments[0].is_type());
assert!(!segments[1].is_type());
}
#[test]
fn test_gts_id_double_tilde_rejected() {
let err = parse_id(>s_id("x.test1.events.type.v1.0~~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("empty segment"), "got: {err}");
}
#[test]
fn test_gts_id_parser_expects_trimmed_input() {
let err = parse_id(&format!(" {} ", gts_id("x.core.events.event.v1~"))).unwrap_err();
assert!(
err.cause
.contains(&format!("must start with '{GTS_ID_PREFIX}'")),
"got: {err}"
);
}
#[test]
fn test_gts_id_trimmed_input() {
let segments = parse_id(>s_id("x.core.events.event.v1~")).unwrap();
assert_eq!(segments.len(), 1);
}
#[test]
fn test_is_uuid_valid() {
assert!(is_uuid("7a1d2f34-5678-49ab-9012-abcdef123456"));
assert!(is_uuid("00000000-0000-0000-0000-000000000000"));
assert!(is_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff"));
}
#[test]
fn test_is_uuid_invalid() {
assert!(!is_uuid("not-a-uuid"));
assert!(!is_uuid("7a1d2f34-5678-49ab-9012-abcdef12345")); assert!(!is_uuid("7a1d2f34-5678-49ab-9012-abcdef1234567")); assert!(!is_uuid("7a1d2f34-5678-49ab-9012-abcdef12345g")); assert!(!is_uuid("7a1d2f3405678-49ab-9012-abcdef123456")); }
#[test]
fn test_combined_anonymous_instance_valid() {
let segments = parse_id(>s_id("x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7a1d2f34-5678-49ab-9012-abcdef123456"))
.unwrap();
assert_eq!(segments.len(), 3);
assert!(segments[0].is_type());
assert!(segments[1].is_type());
assert!(segments[2].uuid_tail().is_some());
assert!(!segments[2].is_type());
assert_eq!(segments[2].raw(), "7a1d2f34-5678-49ab-9012-abcdef123456");
}
#[test]
fn test_combined_anonymous_instance_single_prefix_valid() {
let segments = parse_id(>s_id(
"x.core.events.type.v1~7a1d2f34-5678-49ab-9012-abcdef123456",
))
.unwrap();
assert_eq!(segments.len(), 2);
assert!(segments[0].is_type());
assert!(segments[1].uuid_tail().is_some());
}
#[test]
fn test_combined_anonymous_instance_hyphen_in_segments_rejected() {
let err = parse_id(>s_id("x-vendor.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7a1d2f34-5678-49ab-9012-abcdef123456"))
.unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("'-'"), "got: {err}");
}
#[test]
fn test_uuid_alone_without_prefix_rejected() {
let err = parse_id("7a1d2f34-5678-49ab-9012-abcdef123456").unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(
err.cause
.contains(&format!("must start with '{GTS_ID_PREFIX}'")),
"got: {err}"
);
}
#[test]
fn test_uuid_tail_without_preceding_tilde_rejected() {
let err = parse_id(>s_id("7a1d2f34-5678-49ab-9012-abcdef123456")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("'-'"), "got: {err}");
}
#[test]
fn test_single_segment_instance_rejected() {
let err = parse_id(>s_id("x.pkg.ns.type.v1.0")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("Single-segment instance"), "got: {err}");
}
#[test]
fn test_single_segment_wildcard_allowed() {
let segments = parse_pattern(>s_id("a.b.*")).unwrap();
assert_eq!(segments.len(), 1);
assert!(segments[0].is_wildcard());
}
#[test]
fn test_parse_pattern_multistar_rejected() {
let err = parse_pattern(>s_id("*.*.*.*")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("only once"), "got: {err}");
}
#[test]
fn test_parse_pattern_star_not_at_end_rejected() {
let err = parse_pattern(>s_id("*.core.events.event.v1~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("only at the end"), "got: {err}");
}
#[test]
fn test_parse_pattern_version_wildcard_accepted() {
let segments = parse_pattern(>s_id("x.llm.chat.message.v*")).unwrap();
assert_eq!(segments.len(), 1);
assert!(segments[0].is_wildcard());
}
#[test]
fn test_parse_pattern_star_then_tilde_rejected() {
let err = parse_pattern(>s_id("x.llm.chat.message.v1.*~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("only at the end"), "got: {err}");
}
#[test]
fn test_parse_pattern_midchain_wildcard_rejected() {
let err = parse_pattern(>s_id("x.*~a.b.c.d.v1~")).unwrap_err();
assert!(err.segment.is_none(), "expected id-level error, got: {err}");
assert!(err.cause.contains("only at the end"), "got: {err}");
}
#[test]
fn test_parse_pattern_wildcard_rules_off_without_flag() {
let err = parse_id(>s_id("*.*.*.*")).unwrap_err();
assert!(
err.segment.is_some(),
"expected segment-level error, got: {err}"
);
}
}