Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
/// Largest integer JavaScript can hold exactly; beyond it the three languages disagree.
const SAFE_INTEGER: i64 = 9007199254740991;
/// Fold A-Z to a-z and leave every other character alone.
///
/// Deliberately not `to_lowercase()`: full Unicode case mapping differs between
/// JavaScript, Python and Rust (Turkish dotted I, final sigma, sharp s), and a
/// search that finds a row in one service but not in another is worse than one
/// that consistently ignores accents.
pub fn fold_ascii(text: &str) -> String {
text.chars()
.map(|ch| if ch.is_ascii_uppercase() { ch.to_ascii_lowercase() } else { ch })
.collect()
}
/// The searchable text of a value, or None if there is nothing to search.
///
/// Floats are skipped rather than rendered: no decimal form of them is
/// identical in all three languages, so matching on one could not be pinned.
fn searchable_text(value: &Value) -> Option<String> {
match value {
Value::Str(s) => Some(s.clone()),
Value::Bool(b) => Some((if *b { "true" } else { "false" }).to_string()),
Value::Int(i) if i.abs() <= SAFE_INTEGER => Some(i.to_string()),
_ => None,
}
}
/// The records whose text in any of `fields` contains `query`, in input order.
///
/// An empty query returns everything, because "nothing typed yet" means "show
/// me everything" in every search box ever built.
///
/// Records are `Value` because they are open JSON maps; a struct would be a
/// lie about data whose shape differs from one record to the next.
///
/// # Panics
/// Panics if `fields` is empty.
pub fn search_text(records: &[Value], fields: &[String], query: &str) -> Vec<Value> {
// An empty field list that quietly matches nothing is the classic bug that
// reaches users as "search is broken" with no other symptom.
if fields.is_empty() {
panic!("search_text needs at least one field to search");
}
let needle = fold_ascii(query);
if needle.is_empty() {
return records.to_vec();
}
let mut matches: Vec<Value> = Vec::new();
for record in records {
// Substring matching on UTF-8 bytes is the same answer as matching on
// code points, so Rust, Python and JavaScript agree on every string.
let hit = fields.iter().any(|field| match searchable_text(record.get(field)) {
Some(text) => fold_ascii(&text).contains(&needle),
None => false,
});
if hit {
matches.push(record.clone());
}
}
matches
}
pub fn fune_vector(args: &[Value]) -> Value {
// Refuse what the typed signature cannot hold, with the wording TypeScript
// and Python use, rather than let the conversion below quietly change it.
if !matches!(args[2], Value::Str(_)) {
panic!("query must be a string, received {:?}", args[2]);
}
let fields: Vec<String> = args[1]
.as_arr()
.iter()
.map(|v| v.as_str().to_string())
.collect();
Value::Arr(search_text(args[0].as_arr(), &fields, args[2].as_str()))
}