use varar_core::handler::Handler;
use varar_core::matcher::{ResolvedSteps, find_hits, resolve_hits};
use varar_core::offsets::{utf16_index, utf16_len, utf16_slice};
use varar_core::registry::{Registry, add_step, create_registry};
use varar_core::value::Value;
fn reg() -> Registry {
let r = create_registry();
let r = add_step(&r, "I have {int} cukes", "steps.ts", 1, Handler::noop(), None).unwrap();
add_step(&r, "I withdraw {int}", "steps.ts", 5, Handler::noop(), None).unwrap()
}
#[test]
fn find_hits_returns_no_hits_when_nothing_matches() {
assert!(find_hits("hello world", ®()).is_empty());
}
#[test]
fn emph_builtin_matches_single_and_double_asterisks_and_strips_the_delimiters() {
let r = create_registry();
let r = add_step(&r, "I mention {emph}", "steps.ts", 1, Handler::noop(), None).unwrap();
let single = find_hits("I mention *Emma*.", &r);
assert_eq!(1, single.len());
assert_eq!(vec![Value::String("Emma".to_string())], single[0].args);
let double = find_hits("I mention **Emma**.", &r);
assert_eq!(1, double.len());
assert_eq!(vec![Value::String("Emma".to_string())], double[0].args);
}
#[test]
fn find_hits_returns_one_hit_per_step_expression_that_matches() {
let hits = find_hits("Given I have 5 cukes in my belly", ®());
assert_eq!(1, hits.len());
assert_eq!("I have {int} cukes", hits[0].expression);
assert_eq!(6, hits[0].match_start);
assert_eq!(20, hits[0].match_end);
assert_eq!(vec![Value::Int(5)], hits[0].args);
}
#[test]
fn find_hits_returns_multiple_hits_when_multiple_expressions_match_non_overlapping_ranges() {
let hits = find_hits("I have 5 cukes and I withdraw 3", ®());
let exprs: Vec<String> = hits.iter().map(|h| h.expression.clone()).collect();
assert_eq!(
vec![
"I have {int} cukes".to_string(),
"I withdraw {int}".to_string()
],
exprs
);
}
#[test]
fn resolve_hits_picks_longest_leftmost_when_ranges_overlap() {
let r = create_registry();
let r = add_step(&r, "I have {int} cukes", "s.ts", 1, Handler::noop(), None).unwrap();
let r =
add_step(&r, "I have {int} cukes in my belly", "s.ts", 2, Handler::noop(), None).unwrap();
let result = resolve_hits(find_hits("I have 5 cukes in my belly", &r));
let ResolvedSteps::Ok(steps) = result else {
panic!("expected Ok")
};
assert_eq!(1, steps.len());
assert_eq!("I have {int} cukes in my belly", steps[0].expression);
}
#[test]
fn resolve_hits_returns_ambiguous_when_same_start_and_same_length_match() {
let r = create_registry();
let r = add_step(&r, "I have {int} cukes", "s.ts", 1, Handler::noop(), None).unwrap();
let r = add_step(&r, "I have {int} {word}", "s.ts", 2, Handler::noop(), None).unwrap();
let result = resolve_hits(find_hits("I have 5 cukes", &r));
let ResolvedSteps::Ambiguous(collisions) = result else {
panic!("expected Ambiguous")
};
assert_eq!(1, collisions.len());
assert_eq!(2, collisions[0].candidates.len());
}
#[test]
fn resolve_hits_returns_all_non_overlapping_hits_left_to_right() {
let r = create_registry();
let r = add_step(&r, "I have {int} cukes", "s.ts", 1, Handler::noop(), None).unwrap();
let r = add_step(&r, "I withdraw {int}", "s.ts", 2, Handler::noop(), None).unwrap();
let result = resolve_hits(find_hits("Given I have 5 cukes and I withdraw 3", &r));
let ResolvedSteps::Ok(steps) = result else {
panic!("expected Ok")
};
let exprs: Vec<String> = steps.iter().map(|h| h.expression.clone()).collect();
assert_eq!(
vec![
"I have {int} cukes".to_string(),
"I withdraw {int}".to_string()
],
exprs
);
}
#[test]
fn param_spans_use_utf16_offsets_across_an_astral_character_no_manual_conversion_needed() {
let r = create_registry();
let r = add_step(&r, "I like {string}", "s.ts", 1, Handler::noop(), None).unwrap();
let sentence = "😀 I like \"tea\"";
let hits = find_hits(sentence, &r);
assert_eq!(1, hits.len());
let hit = &hits[0];
let expected_match_start = utf16_index(sentence, sentence.find("I like").unwrap());
assert_eq!(expected_match_start, hit.match_start);
assert_eq!(utf16_len(sentence), hit.match_end);
assert_eq!(1, hit.param_spans.len());
let span = hit.param_spans[0];
let quote_open = utf16_index(sentence, sentence.find('"').unwrap());
assert_eq!(quote_open, span.start);
assert_eq!(utf16_len(sentence), span.end);
assert_eq!("\"tea\"", utf16_slice(sentence, span.start, span.end));
assert_eq!(vec![Value::from("tea")], hit.args);
}
#[test]
fn a_custom_type_with_capture_groups_passes_each_group_to_parse() {
use std::rc::Rc;
use varar_core::registry::define_parameter_type;
let r = define_parameter_type(
&create_registry(),
"range",
r"(\d+)-(\d+)",
Rc::new(|groups: &[&str]| {
Value::list(groups.iter().map(|g| Value::from(*g)).collect::<Vec<_>>())
}),
);
let r = add_step(&r, "the range is {range}", "s.rs", 1, Handler::noop(), None).unwrap();
let hits = find_hits("the range is 10-20", &r);
assert_eq!(1, hits.len());
assert_eq!(vec![Value::list(vec![Value::from("10"), Value::from("20")])], hits[0].args);
}
#[test]
fn a_custom_type_without_groups_still_receives_the_whole_match() {
use std::rc::Rc;
use varar_core::registry::define_parameter_type;
let r = define_parameter_type(
&create_registry(),
"airport",
"[A-Z]{3}",
Rc::new(|groups: &[&str]| {
assert_eq!(1, groups.len());
Value::from(groups[0].to_lowercase())
}),
);
let r = add_step(&r, "I fly to {airport}", "s.rs", 1, Handler::noop(), None).unwrap();
let hits = find_hits("I fly to LHR", &r);
assert_eq!(vec![Value::from("lhr")], hits[0].args);
}