#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedCandidate {
pub gts_id: String,
pub original: String,
}
pub fn normalize_candidate(raw: &str) -> Result<NormalizedCandidate, String> {
let mut trimmed = raw.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
{
trimmed = &trimmed[1..trimmed.len() - 1];
}
let gts_id = if let Some(stripped) = trimmed.strip_prefix("gts://") {
if stripped.contains('#') || stripped.contains('?') {
return Err(format!(
"gts:// URI must not contain fragments (#) or query strings (?): '{raw}'"
));
}
stripped.to_owned()
} else {
trimmed.to_owned()
};
if !gts_id.starts_with("gts.") {
return Err(format!("Does not start with 'gts.': '{raw}'"));
}
Ok(NormalizedCandidate {
gts_id,
original: raw.to_owned(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_gts_uri() {
let result = normalize_candidate("gts://gts.x.core.type.v1~").unwrap();
assert_eq!(result.gts_id, "gts.x.core.type.v1~");
assert_eq!(result.original, "gts://gts.x.core.type.v1~");
}
#[test]
fn test_normalize_plain_gts_id() {
let result = normalize_candidate("gts.x.core.type.v1~").unwrap();
assert_eq!(result.gts_id, "gts.x.core.type.v1~");
assert_eq!(result.original, "gts.x.core.type.v1~");
}
#[test]
fn test_normalize_with_whitespace() {
let result = normalize_candidate(" gts.x.core.type.v1~ ").unwrap();
assert_eq!(result.gts_id, "gts.x.core.type.v1~");
}
#[test]
fn test_normalize_with_quotes() {
let result = normalize_candidate("\"gts.x.core.type.v1~\"").unwrap();
assert_eq!(result.gts_id, "gts.x.core.type.v1~");
let result = normalize_candidate("'gts.x.core.type.v1~'").unwrap();
assert_eq!(result.gts_id, "gts.x.core.type.v1~");
}
#[test]
fn test_reject_fragment() {
let result = normalize_candidate("gts://gts.x.core.type.v1~#foo");
assert!(result.is_err());
assert!(result.unwrap_err().contains("fragments (#)"));
}
#[test]
fn test_reject_query_string() {
let result = normalize_candidate("gts://gts.x.core.type.v1~?bar=1");
assert!(result.is_err());
assert!(result.unwrap_err().contains("query strings (?)"));
}
#[test]
fn test_reject_no_gts_prefix() {
let result = normalize_candidate("x.core.type.v1~");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Does not start with 'gts.'"));
}
#[test]
fn test_normalize_chained_id() {
let result =
normalize_candidate("gts.x.core.events.type.v1~ven.app._.custom_event.v1~").unwrap();
assert_eq!(
result.gts_id,
"gts.x.core.events.type.v1~ven.app._.custom_event.v1~"
);
}
#[test]
fn test_normalize_with_wildcard() {
let result = normalize_candidate("gts.x.core.*").unwrap();
assert_eq!(result.gts_id, "gts.x.core.*");
}
}