use crate::lex::error::{LexError, SourcePos};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
pub type_name: Option<String>,
pub id: String,
}
impl Reference {
#[inline]
pub fn local(id: impl Into<String>) -> Self {
Self {
type_name: None,
id: id.into(),
}
}
#[inline]
pub fn qualified(type_name: impl Into<String>, id: impl Into<String>) -> Self {
Self {
type_name: Some(type_name.into()),
id: id.into(),
}
}
#[inline]
pub fn is_qualified(&self) -> bool {
self.type_name.is_some()
}
#[inline]
pub fn is_local(&self) -> bool {
self.type_name.is_none()
}
}
impl std::fmt::Display for Reference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.type_name {
Some(type_name) => write!(f, "@{}:{}", type_name, self.id),
None => write!(f, "@{}", self.id),
}
}
}
#[inline]
pub fn is_valid_key_token(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !first.is_ascii_lowercase() && first != b'_' {
return false;
}
bytes[1..]
.iter()
.all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
#[inline]
pub fn is_valid_type_name(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
if !bytes[0].is_ascii_uppercase() {
return false;
}
bytes[1..].iter().all(|&b| b.is_ascii_alphanumeric())
}
#[inline]
pub fn is_valid_id_token(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !first.is_ascii_alphabetic() && first != b'_' {
return false;
}
bytes[1..]
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}
pub fn parse_reference(s: &str) -> Result<Reference, LexError> {
parse_reference_at(s, SourcePos::default())
}
pub fn parse_reference_at(s: &str, pos: SourcePos) -> Result<Reference, LexError> {
let s = s.strip_prefix('@').unwrap_or(s);
if let Some((type_part, id_part)) = s.split_once(':') {
if !is_valid_type_name(type_part) {
return Err(LexError::InvalidReference {
message: format!("invalid type name: {}", type_part),
pos,
});
}
if !is_valid_id_token(id_part) {
return Err(LexError::InvalidReference {
message: format!("invalid ID: {}", id_part),
pos,
});
}
Ok(Reference {
type_name: Some(type_part.to_string()),
id: id_part.to_string(),
})
} else {
if !is_valid_id_token(s) {
return Err(LexError::InvalidReference {
message: format!("invalid ID: {}", s),
pos,
});
}
Ok(Reference {
type_name: None,
id: s.to_string(),
})
}
}
#[inline]
pub fn is_list_start(c: char) -> bool {
c == '('
}
#[inline]
pub fn is_list_end(c: char) -> bool {
c == ')'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_token_valid_basic() {
assert!(is_valid_key_token("name"));
assert!(is_valid_key_token("user_id"));
assert!(is_valid_key_token("_private"));
assert!(is_valid_key_token("item123"));
assert!(is_valid_key_token("a"));
assert!(is_valid_key_token("_"));
assert!(is_valid_key_token("abc_def_ghi"));
assert!(is_valid_key_token("a1b2c3"));
}
#[test]
fn test_key_token_valid_edge_cases() {
assert!(is_valid_key_token("a"));
assert!(is_valid_key_token("z"));
assert!(is_valid_key_token("_"));
assert!(is_valid_key_token("_a"));
assert!(is_valid_key_token("_1"));
assert!(is_valid_key_token("__"));
assert!(is_valid_key_token("a_very_long_key_name_with_numbers_123"));
assert!(is_valid_key_token("abcdefghijklmnopqrstuvwxyz"));
assert!(is_valid_key_token("a0123456789"));
}
#[test]
fn test_key_token_invalid() {
assert!(!is_valid_key_token(""));
assert!(!is_valid_key_token("Name"));
assert!(!is_valid_key_token("NAME"));
assert!(!is_valid_key_token("123item"));
assert!(!is_valid_key_token("my-key"));
assert!(!is_valid_key_token("my.key"));
assert!(!is_valid_key_token("my key"));
}
#[test]
fn test_type_name_valid() {
assert!(is_valid_type_name("User"));
assert!(is_valid_type_name("Post123"));
assert!(is_valid_type_name("A"));
assert!(is_valid_type_name("MyType"));
assert!(is_valid_type_name("ABCDef"));
assert!(is_valid_type_name("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
}
#[test]
fn test_type_name_invalid() {
assert!(!is_valid_type_name(""));
assert!(!is_valid_type_name("user"));
assert!(!is_valid_type_name("myType"));
assert!(!is_valid_type_name("User_Type"));
assert!(!is_valid_type_name("_User"));
assert!(!is_valid_type_name("123User"));
}
#[test]
fn test_id_token_valid() {
assert!(is_valid_id_token("user_1"));
assert!(is_valid_id_token("item-two"));
assert!(is_valid_id_token("_system"));
assert!(is_valid_id_token("a"));
assert!(is_valid_id_token("_"));
assert!(is_valid_id_token("my-id"));
assert!(is_valid_id_token("User1"));
assert!(is_valid_id_token("SKU-4020"));
assert!(is_valid_id_token("ABC-DEF-001"));
}
#[test]
fn test_id_token_invalid() {
assert!(!is_valid_id_token(""));
assert!(!is_valid_id_token("123"));
assert!(!is_valid_id_token("-id"));
assert!(!is_valid_id_token("id.name"));
assert!(!is_valid_id_token("id name"));
}
#[test]
fn test_parse_reference_local() {
let r = parse_reference("@user_1").unwrap();
assert_eq!(r.type_name, None);
assert_eq!(r.id, "user_1");
let r = parse_reference("user_1").unwrap();
assert_eq!(r.type_name, None);
assert_eq!(r.id, "user_1");
}
#[test]
fn test_parse_reference_qualified() {
let r = parse_reference("@User:user_1").unwrap();
assert_eq!(r.type_name, Some("User".to_string()));
assert_eq!(r.id, "user_1");
let r = parse_reference("Post:post-123").unwrap();
assert_eq!(r.type_name, Some("Post".to_string()));
assert_eq!(r.id, "post-123");
}
#[test]
fn test_parse_reference_invalid() {
assert!(parse_reference("@").is_err());
assert!(parse_reference("").is_err());
assert!(parse_reference("@:id").is_err());
assert!(parse_reference("@Type:").is_err());
assert!(parse_reference("@user:id").is_err()); assert!(parse_reference("@123User").is_err()); }
#[test]
fn test_reference_constructors() {
let r = Reference::local("my_id");
assert!(r.is_local());
assert!(!r.is_qualified());
let r = Reference::qualified("User", "user_1");
assert!(!r.is_local());
assert!(r.is_qualified());
}
#[test]
fn test_reference_display() {
let r = Reference::local("user_1");
assert_eq!(format!("{}", r), "@user_1");
let r = Reference::qualified("User", "user_1");
assert_eq!(format!("{}", r), "@User:user_1");
}
#[test]
fn test_reference_equality() {
let r1 = Reference::local("id");
let r2 = Reference::local("id");
assert_eq!(r1, r2);
let r3 = Reference::qualified("Type", "id");
assert_ne!(r1, r3);
}
#[test]
fn test_reference_clone() {
let r1 = Reference::qualified("User", "user_1");
let r2 = r1.clone();
assert_eq!(r1, r2);
}
#[test]
fn test_is_list_start_valid() {
assert!(is_list_start('('));
}
#[test]
fn test_is_list_start_invalid() {
assert!(!is_list_start('['));
assert!(!is_list_start('{'));
assert!(!is_list_start(')'));
assert!(!is_list_start('a'));
assert!(!is_list_start('1'));
assert!(!is_list_start(' '));
}
#[test]
fn test_is_list_end_valid() {
assert!(is_list_end(')'));
}
#[test]
fn test_is_list_end_invalid() {
assert!(!is_list_end(']'));
assert!(!is_list_end('}'));
assert!(!is_list_end('('));
assert!(!is_list_end('a'));
assert!(!is_list_end('1'));
assert!(!is_list_end(' '));
}
}