use haz_domain::name::{ProjectName, TagName, TaskName};
use haz_query_lang::expr::RawAtom;
use crate::expr::atom::AtomError;
pub fn parse_tag_atom(atom: RawAtom) -> Result<TagName, AtomError> {
let RawAtom { text, span } = atom;
TagName::try_new(text).map_err(|source| AtomError::InvalidTagName { span, source })
}
pub fn parse_project_atom(atom: RawAtom) -> Result<ProjectName, AtomError> {
let RawAtom { text, span } = atom;
ProjectName::try_new(text).map_err(|source| AtomError::InvalidProjectName { span, source })
}
pub fn parse_task_atom(atom: RawAtom) -> Result<TaskName, AtomError> {
let RawAtom { text, span } = atom;
TaskName::try_new(text).map_err(|source| AtomError::InvalidTaskName { span, source })
}
#[cfg(test)]
mod tests {
use super::*;
use haz_domain::name::NameError;
use haz_query_lang::expr::Expr;
use haz_query_lang::parser::parse;
use haz_query_lang::span::Span;
fn raw(text: &str, start: usize, end: usize) -> RawAtom {
RawAtom {
text: text.to_owned(),
span: Span { start, end },
}
}
#[test]
fn qry_003_parses_valid_tag_atom() {
let tag = parse_tag_atom(raw("backend", 0, 7)).unwrap();
assert_eq!(tag.to_string(), "backend");
}
#[test]
fn qry_003_rejects_invalid_tag_atom_and_records_span() {
let err = parse_tag_atom(raw("with space", 4, 14)).unwrap_err();
match err {
AtomError::InvalidTagName { span, source } => {
assert_eq!(span, Span { start: 4, end: 14 });
assert!(matches!(source, NameError::InvalidChar { c: ' ' }));
}
other => panic!("expected InvalidTagName, got {other:?}"),
}
}
#[test]
fn qry_003_rejects_empty_tag_atom() {
let err = parse_tag_atom(raw("", 7, 7)).unwrap_err();
assert!(matches!(
err,
AtomError::InvalidTagName {
source: NameError::Empty,
..
}
));
}
#[test]
fn qry_003_parses_valid_project_atom() {
let project = parse_project_atom(raw("lib_core", 0, 8)).unwrap();
assert_eq!(project.to_string(), "lib_core");
}
#[test]
fn qry_003_rejects_invalid_project_atom_and_records_span() {
let err = parse_project_atom(raw("-leading", 2, 10)).unwrap_err();
match err {
AtomError::InvalidProjectName { span, source } => {
assert_eq!(span, Span { start: 2, end: 10 });
assert!(matches!(source, NameError::InvalidLeadingChar { c: '-' }));
}
other => panic!("expected InvalidProjectName, got {other:?}"),
}
}
#[test]
fn qry_003_parses_valid_task_atom() {
let task = parse_task_atom(raw("compile", 0, 7)).unwrap();
assert_eq!(task.to_string(), "compile");
}
#[test]
fn qry_003_rejects_invalid_task_atom_and_records_span() {
let err = parse_task_atom(raw("path/segment", 1, 13)).unwrap_err();
match err {
AtomError::InvalidTaskName { span, source } => {
assert_eq!(span, Span { start: 1, end: 13 });
assert!(matches!(source, NameError::InvalidChar { c: '/' }));
}
other => panic!("expected InvalidTaskName, got {other:?}"),
}
}
#[test]
fn qry_003_lifts_parsed_expression_to_typed_tag_expression() {
let expr = parse("backend & !legacy | infra").unwrap();
let typed = expr.try_map(parse_tag_atom).unwrap();
let backend = TagName::try_new("backend").unwrap();
let legacy = TagName::try_new("legacy").unwrap();
let infra = TagName::try_new("infra").unwrap();
let expected = Expr::Or(
Box::new(Expr::And(
Box::new(Expr::Atom(backend)),
Box::new(Expr::Not(Box::new(Expr::Atom(legacy)))),
)),
Box::new(Expr::Atom(infra)),
);
assert_eq!(typed, expected);
}
#[test]
fn qry_003_lifts_parsed_expression_to_typed_project_expression() {
let expr = parse("lib_core | web-frontend").unwrap();
let typed = expr.try_map(parse_project_atom).unwrap();
let lib_core = ProjectName::try_new("lib_core").unwrap();
let web_frontend = ProjectName::try_new("web-frontend").unwrap();
let expected = Expr::Or(
Box::new(Expr::Atom(lib_core)),
Box::new(Expr::Atom(web_frontend)),
);
assert_eq!(typed, expected);
}
#[test]
fn qry_003_lifts_parsed_expression_to_typed_task_expression() {
let expr = parse("!(test_unit & test_integration)").unwrap();
let typed = expr.try_map(parse_task_atom).unwrap();
let unit = TaskName::try_new("test_unit").unwrap();
let integration = TaskName::try_new("test_integration").unwrap();
let expected = Expr::Not(Box::new(Expr::And(
Box::new(Expr::Atom(unit)),
Box::new(Expr::Atom(integration)),
)));
assert_eq!(typed, expected);
}
#[test]
fn qry_003_lift_propagates_first_invalid_atom_with_correct_span() {
let expr = parse("good | foo.bar | other").unwrap();
let err = expr.try_map(parse_tag_atom).unwrap_err();
match err {
AtomError::InvalidTagName { span, source } => {
assert_eq!(span, Span { start: 7, end: 14 });
assert!(matches!(source, NameError::InvalidChar { c: '.' }));
}
other => panic!("expected InvalidTagName, got {other:?}"),
}
}
#[test]
fn atom_error_display_includes_span_and_underlying_message() {
let err = parse_tag_atom(raw("foo.bar", 2, 9)).unwrap_err();
let rendered = err.to_string();
assert!(
rendered.contains("bytes 2..9"),
"missing span in: {rendered}"
);
assert!(
rendered.contains('.'),
"missing source detail in: {rendered}"
);
}
}