use std::collections::BTreeSet;
use crate::content::node::PropertyState;
use crate::content::property::{PropertyType, PropertyValue};
use crate::index::value_pattern::ValuePattern;
use crate::index::{IndexResult, values_of};
pub const EMPTY_VALUE_TOKEN: &str = ":";
pub const MAXIMUM_KEY_SOURCE_UNITS: usize = 100;
pub fn keys_for_property(
property: &PropertyState,
pattern: &ValuePattern,
definition_path: &str,
) -> IndexResult<BTreeSet<String>> {
if property.property_type == PropertyType::Binary {
return Ok(BTreeSet::new());
}
let mut keys = BTreeSet::new();
for value in values_of(property) {
let Some(text) = value.as_text() else {
continue;
};
if pattern.matches(&text, definition_path)? {
keys.insert(encode_key(&text));
}
}
Ok(keys)
}
#[must_use]
pub fn encode_key(value: &str) -> String {
if value.is_empty() {
return EMPTY_VALUE_TOKEN.to_owned();
}
crate::java::url_encode(truncated_units(value))
}
fn truncated_units(value: &str) -> impl Iterator<Item = u16> + '_ {
value.encode_utf16().take(MAXIMUM_KEY_SOURCE_UNITS)
}
#[must_use]
pub fn key_matches_value(key: &str, value: &PropertyValue) -> bool {
value.as_text().is_some_and(|text| encode_key(&text) == key)
}
#[cfg(test)]
mod tests {
use super::{encode_key, keys_for_property};
use crate::content::node::{PropertyState, PropertyValues};
use crate::content::property::{PropertyType, PropertyValue};
use crate::index::value_pattern::ValuePattern;
const PATH: &str = "/oak:index/test";
fn strings(values: &[&str]) -> PropertyState {
PropertyState {
name: "foo".to_owned(),
property_type: PropertyType::String,
values: PropertyValues::Multiple(
values
.iter()
.map(|value| PropertyValue::String((*value).to_owned()))
.collect(),
),
}
}
fn keys(property: &PropertyState) -> Vec<String> {
keys_for_property(property, &ValuePattern::default(), PATH)
.expect("no pattern refusal")
.into_iter()
.collect()
}
#[test]
fn an_empty_value_keys_as_the_empty_token() {
assert_eq!(encode_key(""), ":");
}
#[test]
fn a_colon_in_a_value_is_encoded_rather_than_left_as_the_empty_token() {
assert_eq!(encode_key(":"), "%3A");
}
#[test]
fn a_value_of_exactly_a_hundred_units_is_not_truncated() {
let value = "a".repeat(100);
assert_eq!(encode_key(&value), value);
}
#[test]
fn a_value_of_a_hundred_and_one_units_is_truncated_to_a_hundred() {
let value = "a".repeat(101);
assert_eq!(encode_key(&value), "a".repeat(100));
}
#[test]
fn truncation_inside_a_surrogate_pair_encodes_the_orphan_as_a_question_mark() {
let value = format!("{}{}", "a".repeat(99), '\u{1f600}');
let encoded = encode_key(&value);
assert_eq!(encoded, format!("{}%3F", "a".repeat(99)));
}
#[test]
fn two_values_collapsing_to_one_key_contribute_one_entry() {
let shared = "b".repeat(100);
let property = strings(&[&format!("{shared}one"), &format!("{shared}two")]);
assert_eq!(keys(&property), [shared]);
}
#[test]
fn two_empty_values_contribute_one_entry() {
assert_eq!(keys(&strings(&["", ""])), [":"]);
}
#[test]
fn a_multi_valued_property_contributes_every_distinct_key() {
assert_eq!(keys(&strings(&["a", "b"])), ["a", "b"]);
}
#[test]
fn a_binary_property_contributes_nothing() {
let property = PropertyState {
name: "jcr:data".to_owned(),
property_type: PropertyType::Binary,
values: PropertyValues::Single(PropertyValue::String("ignored".to_owned())),
};
assert!(keys(&property).is_empty());
}
#[test]
fn an_empty_multi_valued_property_contributes_nothing() {
assert!(keys(&strings(&[])).is_empty());
}
#[test]
fn a_value_the_pattern_rejects_contributes_nothing() {
let pattern = ValuePattern::new(Some(vec!["keep".to_owned()]), None, None);
let property = strings(&["keep-me", "drop-me"]);
let keys: Vec<String> = keys_for_property(&property, &pattern, PATH)
.expect("no refusal")
.into_iter()
.collect();
assert_eq!(keys, ["keep-me"]);
}
#[test]
fn a_regular_expression_the_rule_reaches_is_propagated_rather_than_swallowed() {
let pattern = ValuePattern::new(None, None, Some("x.*".to_owned()));
let error = keys_for_property(&strings(&["anything"]), &pattern, PATH)
.expect_err("the pattern is reached");
assert!(
matches!(
error,
crate::index::IndexError::UnsupportedValuePattern { .. }
),
"{error}"
);
}
}