use super::error::{LexError, SourcePos};
use super::span::Span;
use super::tokens::{is_valid_key_token, is_valid_type_name};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructDirective {
pub type_name: String,
pub columns: Vec<String>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AliasDirective {
pub key: String,
pub value: String,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NestDirective {
pub parent_type: String,
pub child_type: String,
pub span: Span,
}
pub fn parse_struct(payload: &str) -> Result<StructDirective, LexError> {
parse_struct_with_span(payload, Span::synthetic())
}
pub fn parse_struct_with_span(payload: &str, span: Span) -> Result<StructDirective, LexError> {
let parts: Vec<&str> = payload.splitn(2, ':').map(|s| s.trim()).collect();
if parts.len() != 2 {
return Err(LexError::InvalidToken {
message: "STRUCT directive must be: TypeName: [col1, col2, ...]".to_string(),
pos: span.start(),
});
}
let type_name = parts[0];
let column_part = parts[1];
if !is_valid_type_name(type_name) {
return Err(LexError::InvalidToken {
message: format!(
"invalid type name '{}': must be PascalCase [A-Z][A-Za-z0-9]*",
type_name
),
pos: span.start(),
});
}
let columns = parse_column_list_with_span(column_part, span)?;
if columns.is_empty() {
return Err(LexError::InvalidToken {
message: "STRUCT directive must have at least one column".to_string(),
pos: span.start(),
});
}
let mut seen = HashMap::new();
for col in &columns {
if seen.insert(col, ()).is_some() {
return Err(LexError::InvalidToken {
message: format!("duplicate column name '{}' in STRUCT", col),
pos: span.start(),
});
}
}
Ok(StructDirective {
type_name: type_name.to_string(),
columns,
span,
})
}
pub fn parse_alias(payload: &str) -> Result<AliasDirective, LexError> {
parse_alias_with_span(payload, Span::synthetic())
}
pub fn parse_alias_with_span(payload: &str, span: Span) -> Result<AliasDirective, LexError> {
let parts: Vec<&str> = payload.splitn(2, ':').map(|s| s.trim()).collect();
if parts.len() != 2 {
return Err(LexError::InvalidToken {
message: "ALIAS directive must be: %key: \"value\"".to_string(),
pos: span.start(),
});
}
let key_part = parts[0];
let value_part = parts[1];
if !key_part.starts_with('%') {
return Err(LexError::InvalidToken {
message: format!("ALIAS key must start with %: got '{}'", key_part),
pos: span.start(),
});
}
let key = &key_part[1..];
if !is_valid_key_token(key) {
return Err(LexError::InvalidToken {
message: format!("invalid ALIAS key '{}': must be [a-z_][a-z0-9_]*", key),
pos: span.start(),
});
}
let value = parse_quoted_string_with_span(value_part, span)?;
Ok(AliasDirective {
key: key.to_string(),
value,
span,
})
}
pub fn parse_nest(payload: &str) -> Result<NestDirective, LexError> {
parse_nest_with_span(payload, Span::synthetic())
}
pub fn parse_nest_with_span(payload: &str, span: Span) -> Result<NestDirective, LexError> {
let parts: Vec<&str> = payload.split('>').map(|s| s.trim()).collect();
if parts.len() != 2 {
return Err(LexError::InvalidToken {
message: "NEST directive must be: ParentType > ChildType".to_string(),
pos: span.start(),
});
}
let parent_type = parts[0];
let child_type = parts[1];
if !is_valid_type_name(parent_type) {
return Err(LexError::InvalidToken {
message: format!(
"invalid parent type name '{}': must be PascalCase",
parent_type
),
pos: span.start(),
});
}
if !is_valid_type_name(child_type) {
return Err(LexError::InvalidToken {
message: format!(
"invalid child type name '{}': must be PascalCase",
child_type
),
pos: span.start(),
});
}
Ok(NestDirective {
parent_type: parent_type.to_string(),
child_type: child_type.to_string(),
span,
})
}
fn parse_column_list_with_span(s: &str, span: Span) -> Result<Vec<String>, LexError> {
let s = s.trim();
if !s.starts_with('[') || !s.ends_with(']') {
return Err(LexError::InvalidToken {
message: "column list must be enclosed in []".to_string(),
pos: span.start(),
});
}
let content = &s[1..s.len() - 1].trim();
if content.is_empty() {
return Ok(Vec::new());
}
let mut columns = Vec::new();
for part in content.split(',') {
let col = part.trim();
if col.is_empty() {
return Err(LexError::InvalidToken {
message: "trailing comma or empty column name in column list".to_string(),
pos: span.start(),
});
}
if !is_valid_key_token(col) {
return Err(LexError::InvalidToken {
message: format!("invalid column name '{}': must be [a-z_][a-z0-9_]*", col),
pos: span.start(),
});
}
columns.push(col.to_string());
}
Ok(columns)
}
fn parse_quoted_string_with_span(s: &str, span: Span) -> Result<String, LexError> {
let s = s.trim();
if !s.starts_with('"') || !s.ends_with('"') {
return Err(LexError::InvalidToken {
message: "value must be a quoted string".to_string(),
pos: span.start(),
});
}
let content = &s[1..s.len() - 1];
let mut result = String::new();
let mut chars = content.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '"' {
if chars.peek() == Some(&'"') {
chars.next();
result.push('"');
} else {
return Err(LexError::UnclosedQuote { pos: span.start() });
}
} else if ch == '\\' {
if let Some(&next_ch) = chars.peek() {
match next_ch {
'n' => {
chars.next();
result.push('\n');
}
't' => {
chars.next();
result.push('\t');
}
'\\' => {
chars.next();
result.push('\\');
}
'"' => {
chars.next();
result.push('"');
}
_ => {
return Err(LexError::InvalidEscape {
sequence: format!("\\{}", next_ch),
pos: SourcePos::new(span.start().line(), span.start().column()),
});
}
}
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_struct_basic() {
let s = parse_struct("User: [id, name, email]").unwrap();
assert_eq!(s.type_name, "User");
assert_eq!(s.columns, vec!["id", "name", "email"]);
}
#[test]
fn test_parse_struct_with_spaces() {
let s = parse_struct("User: [ id , name , email ]").unwrap();
assert_eq!(s.type_name, "User");
assert_eq!(s.columns, vec!["id", "name", "email"]);
}
#[test]
fn test_parse_struct_no_spaces() {
let s = parse_struct("User: [id,name,email]").unwrap();
assert_eq!(s.type_name, "User");
assert_eq!(s.columns, vec!["id", "name", "email"]);
}
#[test]
fn test_parse_struct_invalid_type_name() {
let result = parse_struct("user: [id, name]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_invalid_column_name() {
let result = parse_struct("User: [id, Name]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_duplicate_columns() {
let result = parse_struct("User: [id, name, id]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_empty() {
let result = parse_struct("User: []");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_trailing_comma() {
let result = parse_struct("User: [id, name,]");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_basic() {
let a = parse_alias("%active: \"true\"").unwrap();
assert_eq!(a.key, "active");
assert_eq!(a.value, "true");
}
#[test]
fn test_parse_alias_empty_value() {
let a = parse_alias("%empty: \"\"").unwrap();
assert_eq!(a.key, "empty");
assert_eq!(a.value, "");
}
#[test]
fn test_parse_alias_escaped_quotes() {
let a = parse_alias("%name: \"John \"\"Doc\"\" Doe\"").unwrap();
assert_eq!(a.key, "name");
assert_eq!(a.value, "John \"Doc\" Doe");
}
#[test]
fn test_parse_alias_backslash_quote_escape() {
let a = parse_alias(r#"%name: "say \"hello\"""#).unwrap();
assert_eq!(a.key, "name");
assert_eq!(a.value, "say \"hello\"");
}
#[test]
fn test_parse_alias_backslash_newline_escape() {
let a = parse_alias(r#"%msg: "line1\nline2""#).unwrap();
assert_eq!(a.value, "line1\nline2");
}
#[test]
fn test_parse_alias_backslash_tab_escape() {
let a = parse_alias(r#"%data: "col1\tcol2""#).unwrap();
assert_eq!(a.value, "col1\tcol2");
}
#[test]
fn test_parse_alias_backslash_carriage_return_invalid() {
let result = parse_alias(r#"%crlf: "line\r\n""#);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
LexError::InvalidEscape { .. }
));
}
#[test]
fn test_parse_alias_backslash_backslash_escape() {
let a = parse_alias(r#"%path: "C:\\Users\\test""#).unwrap();
assert_eq!(a.value, "C:\\Users\\test");
}
#[test]
fn test_parse_alias_unknown_backslash_escape_error() {
let result = parse_alias(r#"%unknown: "test\x""#);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
LexError::InvalidEscape { .. }
));
}
#[test]
fn test_parse_alias_no_percent() {
let result = parse_alias("active: \"true\"");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_not_quoted() {
let result = parse_alias("%active: true");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_invalid_key() {
let result = parse_alias("%Active: \"true\"");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_basic() {
let n = parse_nest("User > Post").unwrap();
assert_eq!(n.parent_type, "User");
assert_eq!(n.child_type, "Post");
}
#[test]
fn test_parse_nest_with_spaces() {
let n = parse_nest(" User > Post ").unwrap();
assert_eq!(n.parent_type, "User");
assert_eq!(n.child_type, "Post");
}
#[test]
fn test_parse_nest_invalid_parent() {
let result = parse_nest("user > Post");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_invalid_child() {
let result = parse_nest("User > post");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_no_separator() {
let result = parse_nest("User Post");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_single_column() {
let s = parse_struct("Item: [value]").unwrap();
assert_eq!(s.type_name, "Item");
assert_eq!(s.columns, vec!["value"]);
}
#[test]
fn test_parse_struct_many_columns() {
let s = parse_struct("Record: [a, b, c, d, e, f, g, h, i, j]").unwrap();
assert_eq!(s.type_name, "Record");
assert_eq!(s.columns.len(), 10);
}
#[test]
fn test_parse_struct_column_with_numbers() {
let s = parse_struct("Data: [col1, col2, value123]").unwrap();
assert_eq!(s.columns, vec!["col1", "col2", "value123"]);
}
#[test]
fn test_parse_struct_column_with_underscore() {
let s = parse_struct("Data: [first_name, last_name, _private]").unwrap();
assert_eq!(s.columns, vec!["first_name", "last_name", "_private"]);
}
#[test]
fn test_parse_struct_pascal_case_with_numbers() {
let s = parse_struct("User2: [id]").unwrap();
assert_eq!(s.type_name, "User2");
}
#[test]
fn test_parse_struct_type_name_lowercase_error() {
let result = parse_struct("user: [id]");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("PascalCase"));
}
#[test]
fn test_parse_struct_type_name_with_underscore_error() {
let result = parse_struct("User_Type: [id]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_column_uppercase_error() {
let result = parse_struct("User: [Id, Name]");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("column name"));
}
#[test]
fn test_parse_struct_column_with_hyphen_error() {
let result = parse_struct("User: [first-name]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_missing_brackets() {
let result = parse_struct("User: id, name");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("enclosed in []"));
}
#[test]
fn test_parse_struct_missing_colon() {
let result = parse_struct("User [id, name]");
assert!(result.is_err());
}
#[test]
fn test_parse_struct_double_comma() {
let result = parse_struct("User: [id,, name]");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("empty column"));
}
#[test]
fn test_parse_struct_equality() {
let a = StructDirective {
type_name: "User".to_string(),
columns: vec!["id".to_string(), "name".to_string()],
span: Span::synthetic(),
};
let b = StructDirective {
type_name: "User".to_string(),
columns: vec!["id".to_string(), "name".to_string()],
span: Span::synthetic(),
};
assert_eq!(a, b);
}
#[test]
fn test_parse_struct_debug() {
let s = StructDirective {
type_name: "User".to_string(),
columns: vec!["id".to_string()],
span: Span::synthetic(),
};
let debug = format!("{:?}", s);
assert!(debug.contains("User"));
assert!(debug.contains("id"));
}
#[test]
fn test_parse_alias_with_spaces_in_value() {
let a = parse_alias("%greeting: \"Hello, World!\"").unwrap();
assert_eq!(a.key, "greeting");
assert_eq!(a.value, "Hello, World!");
}
#[test]
fn test_parse_alias_with_unicode() {
let a = parse_alias("%emoji: \"Hello 🌍\"").unwrap();
assert_eq!(a.value, "Hello 🌍");
}
#[test]
fn test_parse_alias_with_numbers_in_key() {
let a = parse_alias("%config123: \"value\"").unwrap();
assert_eq!(a.key, "config123");
}
#[test]
fn test_parse_alias_with_underscore_in_key() {
let a = parse_alias("%my_config: \"value\"").unwrap();
assert_eq!(a.key, "my_config");
}
#[test]
fn test_parse_alias_key_starting_with_number_error() {
let result = parse_alias("%123key: \"value\"");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_key_with_hyphen_error() {
let result = parse_alias("%my-key: \"value\"");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_value_with_colons() {
let a = parse_alias("%url: \"https://example.com:8080\"").unwrap();
assert_eq!(a.value, "https://example.com:8080");
}
#[test]
fn test_parse_alias_missing_key_percent() {
let result = parse_alias("key: \"value\"");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("must start with %"));
}
#[test]
fn test_parse_alias_unquoted_value_error() {
let result = parse_alias("%key: value");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("quoted string"));
}
#[test]
fn test_parse_alias_single_quote_error() {
let result = parse_alias("%key: 'value'");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_unclosed_quote_error() {
let result = parse_alias("%key: \"unclosed");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_unescaped_quote_in_value_error() {
let result = parse_alias("%key: \"say \"hello\"\"");
assert!(result.is_err());
}
#[test]
fn test_parse_alias_equality() {
let a = AliasDirective {
key: "test".to_string(),
value: "value".to_string(),
span: Span::synthetic(),
};
let b = AliasDirective {
key: "test".to_string(),
value: "value".to_string(),
span: Span::synthetic(),
};
assert_eq!(a, b);
}
#[test]
fn test_parse_alias_debug() {
let a = AliasDirective {
key: "test".to_string(),
value: "value".to_string(),
span: Span::synthetic(),
};
let debug = format!("{:?}", a);
assert!(debug.contains("test"));
assert!(debug.contains("value"));
}
#[test]
fn test_parse_nest_long_type_names() {
let n = parse_nest("VeryLongParentTypeName > VeryLongChildTypeName").unwrap();
assert_eq!(n.parent_type, "VeryLongParentTypeName");
assert_eq!(n.child_type, "VeryLongChildTypeName");
}
#[test]
fn test_parse_nest_type_with_numbers() {
let n = parse_nest("User2 > Post2").unwrap();
assert_eq!(n.parent_type, "User2");
assert_eq!(n.child_type, "Post2");
}
#[test]
fn test_parse_nest_no_spaces() {
let n = parse_nest("User>Post").unwrap();
assert_eq!(n.parent_type, "User");
assert_eq!(n.child_type, "Post");
}
#[test]
fn test_parse_nest_extra_spaces() {
let n = parse_nest(" User > Post ").unwrap();
assert_eq!(n.parent_type, "User");
assert_eq!(n.child_type, "Post");
}
#[test]
fn test_parse_nest_multiple_arrows_error() {
let result = parse_nest("A > B > C");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_empty_parent_error() {
let result = parse_nest("> Child");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_empty_child_error() {
let result = parse_nest("Parent >");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_lowercase_parent_error() {
let result = parse_nest("parent > Child");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("parent type"));
}
#[test]
fn test_parse_nest_lowercase_child_error() {
let result = parse_nest("Parent > child");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{}", err).contains("child type"));
}
#[test]
fn test_parse_nest_snake_case_error() {
let result = parse_nest("User_Type > Post_Type");
assert!(result.is_err());
}
#[test]
fn test_parse_nest_equality() {
let a = NestDirective {
parent_type: "User".to_string(),
child_type: "Post".to_string(),
span: Span::synthetic(),
};
let b = NestDirective {
parent_type: "User".to_string(),
child_type: "Post".to_string(),
span: Span::synthetic(),
};
assert_eq!(a, b);
}
#[test]
fn test_parse_nest_debug() {
let n = NestDirective {
parent_type: "User".to_string(),
child_type: "Post".to_string(),
span: Span::synthetic(),
};
let debug = format!("{:?}", n);
assert!(debug.contains("User"));
assert!(debug.contains("Post"));
}
#[test]
fn test_parse_nest_clone() {
let original = NestDirective {
parent_type: "User".to_string(),
child_type: "Post".to_string(),
span: Span::synthetic(),
};
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_parse_column_list_single_underscore() {
let s = parse_struct("Data: [_]").unwrap();
assert_eq!(s.columns, vec!["_"]);
}
#[test]
fn test_parse_column_list_multiple_underscores() {
let s = parse_struct("Data: [_, __, ___]").unwrap();
assert_eq!(s.columns, vec!["_", "__", "___"]);
}
#[test]
fn test_parse_column_list_leading_underscore() {
let s = parse_struct("Data: [_private, __internal]").unwrap();
assert_eq!(s.columns, vec!["_private", "__internal"]);
}
}