paginate-core 0.1.0

Pure, language-agnostic pagination / filter / sort / search engine shared by pypaginate (Python) and the future JS/TS port.
Documentation
use super::*;

fn item(pairs: &[(&str, &str)]) -> Value {
    let mut map = BTreeMap::new();
    for (key, value) in pairs {
        map.insert((*key).to_owned(), Value::Str((*value).to_owned()));
    }
    Value::Map(map)
}

fn spec(query: &str, fields: &[&str]) -> SearchSpec {
    SearchSpec {
        query: query.to_owned(),
        fields: fields.iter().map(|s| (*s).to_owned()).collect(),
        weights: None,
        mode: SearchFieldMode::Contains,
        fuzzy: FuzzyMode::Exact,
        threshold: 75,
        min_length: 1,
        max_results: None,
    }
}

#[test]
fn single_field_contains_and_ranks() {
    let items = vec![
        item(&[("name", "Alice")]),
        item(&[("name", "Bob")]),
        item(&[("name", "Alicia")]),
    ];
    let idx = search_indices(&items, &spec("ali", &["name"])).unwrap();
    // Both Alice and Alicia contain "ali"; equal score keeps original order.
    assert_eq!(idx, vec![0, 2]);
}

#[test]
fn all_tokens_must_match() {
    let items = vec![
        item(&[("bio", "rust developer")]),
        item(&[("bio", "rust enthusiast")]),
    ];
    let idx = search_indices(&items, &spec("rust developer", &["bio"])).unwrap();
    assert_eq!(idx, vec![0]);
}

#[test]
fn short_query_returns_all() {
    let items = vec![item(&[("name", "Alice")]), item(&[("name", "Bob")])];
    let mut s = spec("ab", &["name"]);
    s.min_length = 3;
    let idx = search_indices(&items, &s).unwrap();
    assert_eq!(idx, vec![0, 1]);
}

#[test]
fn weighted_multi_field_ranks_by_weight() {
    let items = vec![
        item(&[("name", "zeta"), ("bio", "alpha")]),
        item(&[("name", "alpha"), ("bio", "zeta")]),
    ];
    let mut s = spec("alpha", &["name", "bio"]);
    s.weights = Some(BTreeMap::from([("name".to_owned(), 3.0)]));
    let idx = search_indices(&items, &s).unwrap();
    // Item 1 matches "alpha" in the weighted `name` field -> ranks first.
    assert_eq!(idx, vec![1, 0]);
}

#[test]
fn max_results_caps_output() {
    let items = vec![
        item(&[("name", "aaa")]),
        item(&[("name", "aab")]),
        item(&[("name", "aac")]),
    ];
    let mut s = spec("aa", &["name"]);
    s.max_results = Some(2);
    let idx = search_indices(&items, &s).unwrap();
    assert_eq!(idx.len(), 2);
}

#[test]
fn exact_mode_requires_full_value() {
    let items = vec![item(&[("tag", "rust")]), item(&[("tag", "rustacean")])];
    let mut s = spec("rust", &["tag"]);
    s.mode = SearchFieldMode::Exact;
    let idx = search_indices(&items, &s).unwrap();
    assert_eq!(idx, vec![0]);
}