use crate::parse::{expected_format, is_valid_segment_token, parse_u32_exact};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsIdSegmentParts {
raw: String,
vendor: String,
package: String,
namespace: String,
type_name: String,
ver_major: u32,
ver_minor: Option<u32>,
}
impl GtsIdSegmentParts {
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn vendor(&self) -> &str {
&self.vendor
}
#[must_use]
pub fn package(&self) -> &str {
&self.package
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
#[must_use]
pub fn type_name(&self) -> &str {
&self.type_name
}
#[must_use]
pub fn ver_major(&self) -> u32 {
self.ver_major
}
#[must_use]
pub fn ver_minor(&self) -> Option<u32> {
self.ver_minor
}
#[must_use]
pub fn is_type(&self) -> bool {
self.raw.ends_with('~')
}
}
pub trait SegmentView {
fn vendor(&self) -> &str;
fn package(&self) -> &str;
fn namespace(&self) -> &str;
fn type_name(&self) -> &str;
fn ver_major(&self) -> u32;
fn ver_minor(&self) -> Option<u32>;
fn is_type(&self) -> bool;
fn uuid_tail(&self) -> Option<&str>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsUuidTail(String);
impl GtsUuidTail {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn uuid(&self) -> Option<uuid::Uuid> {
uuid::Uuid::parse_str(&self.0).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GtsIdSegment {
Concrete(GtsIdSegmentParts),
UuidTail(GtsUuidTail),
}
impl GtsIdSegment {
#[must_use]
fn parts(&self) -> Option<&GtsIdSegmentParts> {
match self {
GtsIdSegment::Concrete(p) => Some(p),
GtsIdSegment::UuidTail(_) => None,
}
}
#[must_use]
pub fn raw(&self) -> &str {
match self {
GtsIdSegment::Concrete(p) => &p.raw,
GtsIdSegment::UuidTail(uuid) => uuid.as_str(),
}
}
#[must_use]
pub fn vendor(&self) -> &str {
self.parts().map_or("", |p| &p.vendor)
}
#[must_use]
pub fn package(&self) -> &str {
self.parts().map_or("", |p| &p.package)
}
#[must_use]
pub fn namespace(&self) -> &str {
self.parts().map_or("", |p| &p.namespace)
}
#[must_use]
pub fn type_name(&self) -> &str {
self.parts().map_or("", |p| &p.type_name)
}
#[must_use]
pub fn ver_major(&self) -> u32 {
self.parts().map_or(0, |p| p.ver_major)
}
#[must_use]
pub fn ver_minor(&self) -> Option<u32> {
self.parts().and_then(|p| p.ver_minor)
}
#[must_use]
pub fn is_type(&self) -> bool {
self.raw().ends_with('~')
}
#[must_use]
pub fn uuid_tail(&self) -> Option<&str> {
match self {
GtsIdSegment::UuidTail(uuid) => Some(uuid.as_str()),
GtsIdSegment::Concrete(_) => None,
}
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn uuid(&self) -> Option<uuid::Uuid> {
self.uuid_tail().and_then(|s| uuid::Uuid::parse_str(s).ok())
}
pub(crate) fn uuid_tail_segment(uuid: &str) -> Self {
GtsIdSegment::UuidTail(GtsUuidTail(uuid.to_owned()))
}
pub(crate) fn parse(num: usize, segment: &str) -> Result<Self, String> {
let (parts, _is_wildcard) = parse_segment_parts(num, segment, false)?;
Ok(GtsIdSegment::Concrete(parts))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GtsIdPatternSegment {
Segment(GtsIdSegment),
Wildcard(GtsIdSegmentParts),
}
impl GtsIdPatternSegment {
#[must_use]
pub fn is_wildcard(&self) -> bool {
matches!(self, GtsIdPatternSegment::Wildcard(_))
}
#[must_use]
pub fn raw(&self) -> &str {
match self {
GtsIdPatternSegment::Segment(s) => s.raw(),
GtsIdPatternSegment::Wildcard(p) => &p.raw,
}
}
#[must_use]
pub fn vendor(&self) -> &str {
match self {
GtsIdPatternSegment::Segment(s) => s.vendor(),
GtsIdPatternSegment::Wildcard(p) => &p.vendor,
}
}
#[must_use]
pub fn package(&self) -> &str {
match self {
GtsIdPatternSegment::Segment(s) => s.package(),
GtsIdPatternSegment::Wildcard(p) => &p.package,
}
}
#[must_use]
pub fn namespace(&self) -> &str {
match self {
GtsIdPatternSegment::Segment(s) => s.namespace(),
GtsIdPatternSegment::Wildcard(p) => &p.namespace,
}
}
#[must_use]
pub fn type_name(&self) -> &str {
match self {
GtsIdPatternSegment::Segment(s) => s.type_name(),
GtsIdPatternSegment::Wildcard(p) => &p.type_name,
}
}
#[must_use]
pub fn ver_major(&self) -> u32 {
match self {
GtsIdPatternSegment::Segment(s) => s.ver_major(),
GtsIdPatternSegment::Wildcard(p) => p.ver_major,
}
}
#[must_use]
pub fn ver_minor(&self) -> Option<u32> {
match self {
GtsIdPatternSegment::Segment(s) => s.ver_minor(),
GtsIdPatternSegment::Wildcard(p) => p.ver_minor,
}
}
#[must_use]
pub fn is_type(&self) -> bool {
self.raw().ends_with('~')
}
#[must_use]
pub fn uuid_tail(&self) -> Option<&str> {
match self {
GtsIdPatternSegment::Segment(s) => s.uuid_tail(),
GtsIdPatternSegment::Wildcard(_) => None,
}
}
pub(crate) fn uuid_tail_segment(uuid: &str) -> Self {
GtsIdPatternSegment::Segment(GtsIdSegment::uuid_tail_segment(uuid))
}
pub(crate) fn parse(num: usize, segment: &str) -> Result<Self, String> {
let (parts, is_wildcard) = parse_segment_parts(num, segment, true)?;
if is_wildcard {
Ok(GtsIdPatternSegment::Wildcard(parts))
} else {
Ok(GtsIdPatternSegment::Segment(GtsIdSegment::Concrete(parts)))
}
}
}
fn parse_segment_parts(
num: usize,
segment: &str,
allow_wildcards: bool,
) -> Result<(GtsIdSegmentParts, bool), String> {
let mut seg = segment.to_owned();
if seg.contains('~') {
let tilde_count = seg.matches('~').count();
if tilde_count > 1 {
return Err("Too many '~' characters".to_owned());
}
if seg.ends_with('~') {
seg.pop();
} else {
return Err("'~' must be at the end".to_owned());
}
}
let tokens: Vec<&str> = seg.split('.').collect();
let fmt = expected_format(num);
if tokens.len() > 6 {
return Err(format!(
"Too many tokens (got {}, max 6). Expected format: {fmt}",
tokens.len()
));
}
let ends_with_wildcard = allow_wildcards && seg.ends_with('*');
if !ends_with_wildcard && tokens.len() < 5 {
return Err(format!(
"Too few tokens (got {}, min 5). Expected format: {fmt}",
tokens.len()
));
}
if !ends_with_wildcard && tokens.len() == 6 {
let has_wildcard = allow_wildcards && tokens.contains(&"*");
if !has_wildcard
&& !tokens[4].starts_with('v')
&& tokens[5].starts_with('v')
&& is_valid_segment_token(tokens[4])
{
return Err(format!(
"Too many name tokens before version (got 5, expected 4). Expected format: {fmt}"
));
}
}
for (i, token) in tokens.iter().take(4).enumerate() {
if allow_wildcards && *token == "*" {
if i == tokens.len() - 1 {
break; }
return Err("Wildcard '*' is only allowed as the final token".to_owned());
}
if !is_valid_segment_token(token) {
let token_name = match i {
0 => "vendor",
1 => "package",
2 => "namespace",
3 => "type",
_ => "token",
};
return Err(format!(
"Invalid {token_name} token '{token}'. \
Must start with [a-z_] and contain only [a-z0-9_]"
));
}
}
let mut parts = GtsIdSegmentParts {
raw: segment.to_owned(),
vendor: String::new(),
package: String::new(),
namespace: String::new(),
type_name: String::new(),
ver_major: 0,
ver_minor: None,
};
if !tokens.is_empty() {
if allow_wildcards && tokens[0] == "*" {
return Ok((parts, true));
}
tokens[0].clone_into(&mut parts.vendor);
}
if tokens.len() > 1 {
if allow_wildcards && tokens[1] == "*" {
return Ok((parts, true));
}
tokens[1].clone_into(&mut parts.package);
}
if tokens.len() > 2 {
if allow_wildcards && tokens[2] == "*" {
return Ok((parts, true));
}
tokens[2].clone_into(&mut parts.namespace);
}
if tokens.len() > 3 {
if allow_wildcards && tokens[3] == "*" {
return Ok((parts, true));
}
tokens[3].clone_into(&mut parts.type_name);
}
if tokens.len() > 4 {
if allow_wildcards && tokens[4] == "*" {
if 4 != tokens.len() - 1 {
return Err("Wildcard '*' is only allowed as the final token".to_owned());
}
return Ok((parts, true));
}
if allow_wildcards && tokens[4] == "v*" {
if 4 != tokens.len() - 1 {
return Err("Wildcard '*' is only allowed as the final token".to_owned());
}
return Ok((parts, true));
}
if !tokens[4].starts_with('v') {
return Err("Major version must start with 'v'".to_owned());
}
let major_str = &tokens[4][1..];
parts.ver_major = parse_u32_exact(major_str)
.ok_or_else(|| format!("Major version must be an integer, got '{major_str}'"))?;
}
if tokens.len() > 5 {
if allow_wildcards && tokens[5] == "*" {
return Ok((parts, true));
}
parts.ver_minor = Some(
parse_u32_exact(tokens[5])
.ok_or_else(|| format!("Minor version must be an integer, got '{}'", tokens[5]))?,
);
}
Ok((parts, false))
}
impl SegmentView for GtsIdSegment {
fn vendor(&self) -> &str {
self.vendor()
}
fn package(&self) -> &str {
self.package()
}
fn namespace(&self) -> &str {
self.namespace()
}
fn type_name(&self) -> &str {
self.type_name()
}
fn ver_major(&self) -> u32 {
self.ver_major()
}
fn ver_minor(&self) -> Option<u32> {
self.ver_minor()
}
fn is_type(&self) -> bool {
self.is_type()
}
fn uuid_tail(&self) -> Option<&str> {
self.uuid_tail()
}
}
impl SegmentView for GtsIdPatternSegment {
fn vendor(&self) -> &str {
self.vendor()
}
fn package(&self) -> &str {
self.package()
}
fn namespace(&self) -> &str {
self.namespace()
}
fn type_name(&self) -> &str {
self.type_name()
}
fn ver_major(&self) -> u32 {
self.ver_major()
}
fn ver_minor(&self) -> Option<u32> {
self.ver_minor()
}
fn is_type(&self) -> bool {
self.is_type()
}
fn uuid_tail(&self) -> Option<&str> {
self.uuid_tail()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_valid_segment_basic() {
let parsed = GtsIdSegment::parse(1, "x.core.events.event.v1~").unwrap();
assert_eq!(parsed.vendor(), "x");
assert_eq!(parsed.package(), "core");
assert_eq!(parsed.namespace(), "events");
assert_eq!(parsed.type_name(), "event");
assert_eq!(parsed.ver_major(), 1);
assert_eq!(parsed.ver_minor(), None);
assert!(parsed.is_type());
}
#[test]
fn test_valid_segment_with_minor() {
let parsed = GtsIdSegment::parse(1, "x.core.events.event.v1.2~").unwrap();
assert_eq!(parsed.ver_major(), 1);
assert_eq!(parsed.ver_minor(), Some(2));
}
#[test]
fn test_segment_too_many_tildes() {
let err = GtsIdSegment::parse(1, "x.core.events.event.v1~~").unwrap_err();
assert!(err.contains("Too many '~' characters"), "got: {err}");
}
#[test]
fn test_segment_tilde_not_at_end() {
let err = GtsIdSegment::parse(1, "x.core~mid.events.event.v1").unwrap_err();
assert!(err.contains("'~' must be at the end"), "got: {err}");
}
#[test]
fn test_segment_too_many_tokens() {
let err = GtsIdSegment::parse(1, "x.core.events.event.v1.2.extra~").unwrap_err();
assert!(err.contains("Too many tokens"), "got: {err}");
}
#[test]
fn test_segment_too_few_tokens() {
let err = GtsIdSegment::parse(1, "x.core.events.event~").unwrap_err();
assert!(err.contains("Too few tokens"), "got: {err}");
}
#[test]
fn test_segment_too_many_name_tokens() {
let err = GtsIdSegment::parse(2, "x.core.ns.type.extra.v1~").unwrap_err();
assert!(
err.contains("Too many name tokens before version"),
"got: {err}"
);
}
#[test]
fn test_segment_version_without_v() {
let err = GtsIdSegment::parse(1, "x.core.events.event.1~").unwrap_err();
assert!(
err.contains("Major version must start with 'v'"),
"got: {err}"
);
}
#[test]
fn test_segment_version_not_integer() {
let err = GtsIdSegment::parse(1, "x.core.events.event.vX~").unwrap_err();
assert!(
err.contains("Major version must be an integer"),
"got: {err}"
);
}
#[test]
fn test_segment_version_leading_zeros() {
let err = GtsIdSegment::parse(1, "x.core.events.event.v01~").unwrap_err();
assert!(
err.contains("Major version must be an integer"),
"got: {err}"
);
}
#[test]
fn test_segment_invalid_vendor_token() {
let err = GtsIdSegment::parse(1, "1bad.core.events.event.v1~").unwrap_err();
assert!(err.contains("Invalid vendor token"), "got: {err}");
}
#[test]
fn test_concrete_parse_rejects_wildcard() {
let err = GtsIdSegment::parse(1, "x.*").unwrap_err();
assert!(err.contains("Too few tokens"), "got: {err}");
}
#[test]
fn test_segment1_format_has_gts_prefix() {
let err = GtsIdSegment::parse(1, "x.core.events.event~").unwrap_err();
let expected = format!(
"{}vendor.package.namespace.type.vMAJOR",
crate::GTS_ID_PREFIX
);
assert!(
err.contains(&expected),
"segment #1 format should include configured prefix, got: {err}"
);
}
#[test]
fn test_segment2_format_no_gts_prefix() {
let err = GtsIdSegment::parse(2, "x.core.events.event~").unwrap_err();
assert!(
!err.contains(&format!("{}vendor", crate::GTS_ID_PREFIX)),
"segment #2 format should NOT include configured prefix, got: {err}"
);
assert!(
err.contains("vendor.package.namespace.type.vMAJOR"),
"segment #2 should show vendor.package format, got: {err}"
);
}
#[test]
fn test_wildcard_at_vendor() {
let parsed = GtsIdPatternSegment::parse(1, "*").unwrap();
assert!(parsed.is_wildcard());
}
#[test]
fn test_wildcard_at_package() {
let parsed = GtsIdPatternSegment::parse(1, "x.*").unwrap();
assert!(parsed.is_wildcard());
assert_eq!(parsed.vendor(), "x");
}
#[test]
fn test_pattern_concrete_segment_is_not_wildcard() {
let parsed = GtsIdPatternSegment::parse(1, "x.core.events.event.v1~").unwrap();
assert!(!parsed.is_wildcard());
assert_eq!(parsed.vendor(), "x");
assert!(parsed.is_type());
}
#[test]
fn test_wildcard_invalid_token_before_star() {
let err = GtsIdPatternSegment::parse(1, "1bad.*").unwrap_err();
assert!(err.contains("Invalid vendor token"), "got: {err}");
}
#[test]
fn test_wildcard_in_middle_rejected() {
let err = GtsIdPatternSegment::parse(1, "x.*.ns.type.v1").unwrap_err();
assert!(
err.contains("only allowed as the final token"),
"got: {err}"
);
}
#[test]
fn test_wildcard_at_version_position_not_final() {
let err = GtsIdPatternSegment::parse(1, "x.pkg.ns.type.*.extra").unwrap_err();
assert!(
err.contains("only allowed as the final token"),
"got: {err}"
);
}
#[test]
fn test_glued_version_wildcard() {
let parsed = GtsIdPatternSegment::parse(1, "x.pkg.ns.type.v*").unwrap();
assert!(parsed.is_wildcard());
assert_eq!(parsed.vendor(), "x");
assert_eq!(parsed.package(), "pkg");
assert_eq!(parsed.namespace(), "ns");
assert_eq!(parsed.type_name(), "type");
assert_eq!(parsed.ver_major(), 0);
assert_eq!(parsed.ver_minor(), None);
}
#[test]
fn test_glued_version_wildcard_only_v_star() {
let err = GtsIdPatternSegment::parse(1, "x.pkg.ns.type.v1*").unwrap_err();
assert!(
err.contains("Major version must be an integer"),
"got: {err}"
);
}
#[test]
fn test_glued_version_wildcard_rejected_at_minor() {
let err = GtsIdPatternSegment::parse(1, "x.pkg.ns.type.v1.v*").unwrap_err();
assert!(
err.contains("Minor version must be an integer"),
"got: {err}"
);
}
#[test]
fn test_glued_version_wildcard_rejected_for_concrete() {
let err = GtsIdSegment::parse(1, "x.pkg.ns.type.v*").unwrap_err();
assert!(
err.contains("Major version must be an integer"),
"got: {err}"
);
}
#[test]
fn test_uuid_tail_segment_accessors() {
const UUID_TAIL: &str = "7a1d2f34-5678-49ab-9012-abcdef123456";
let seg = GtsIdSegment::uuid_tail_segment(UUID_TAIL);
assert_eq!(seg.uuid_tail(), Some(UUID_TAIL));
assert_eq!(seg.raw(), UUID_TAIL);
assert!(!seg.is_type());
assert_eq!(seg.vendor(), "");
assert_eq!(seg.ver_major(), 0);
assert_eq!(seg.ver_minor(), None);
#[cfg(feature = "uuid")]
{
let expected = uuid::Uuid::parse_str(UUID_TAIL).ok();
assert_eq!(seg.uuid(), expected);
let GtsIdSegment::UuidTail(tail) = &seg else {
panic!("expected uuid-tail segment");
};
assert_eq!(tail.uuid(), expected);
}
}
#[test]
fn test_concrete_and_wildcard_have_no_uuid_tail() {
let concrete = GtsIdSegment::parse(1, "x.core.events.event.v1~").unwrap();
assert_eq!(concrete.uuid_tail(), None);
#[cfg(feature = "uuid")]
assert_eq!(concrete.uuid(), None);
let wildcard = GtsIdPatternSegment::parse(1, "x.*").unwrap();
assert_eq!(wildcard.uuid_tail(), None);
}
#[test]
fn test_segment_parts_accessors() {
let concrete = GtsIdSegment::parse(1, "x.core.events.event.v1.2~").unwrap();
let GtsIdSegment::Concrete(parts) = concrete else {
panic!("expected concrete segment");
};
assert_eq!(parts.raw(), "x.core.events.event.v1.2~");
assert_eq!(parts.vendor(), "x");
assert_eq!(parts.package(), "core");
assert_eq!(parts.namespace(), "events");
assert_eq!(parts.type_name(), "event");
assert_eq!(parts.ver_major(), 1);
assert_eq!(parts.ver_minor(), Some(2));
assert!(parts.is_type());
}
}