use crate::content::node::{NodeState, PropertyState};
use crate::index::{IndexError, IndexResult, IndexWarning, strict_string, strict_strings};
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ValuePattern {
included_prefixes: Option<Vec<String>>,
excluded_prefixes: Option<Vec<String>>,
regular_expression: Option<String>,
}
impl ValuePattern {
pub fn from_definition(
definition: &NodeState<'_>,
definition_path: &str,
warnings: &mut Vec<IndexWarning>,
) -> IndexResult<Self> {
let included = read_prefixes(
definition,
"valueIncludedPrefixes",
definition_path,
warnings,
)?;
let excluded = read_prefixes(
definition,
"valueExcludedPrefixes",
definition_path,
warnings,
)?;
let regular_expression = definition
.property("valuePattern")?
.as_ref()
.and_then(|property| strict_string(Some(property)).map(str::to_owned));
if let Some(pattern) = ®ular_expression {
warnings.push(IndexWarning::ValuePatternNotEvaluated {
pattern: pattern.clone(),
});
}
Ok(Self {
included_prefixes: included,
excluded_prefixes: excluded,
regular_expression,
})
}
#[must_use]
pub fn new(
included_prefixes: Option<Vec<String>>,
excluded_prefixes: Option<Vec<String>>,
regular_expression: Option<String>,
) -> Self {
Self {
included_prefixes,
excluded_prefixes,
regular_expression,
}
}
#[must_use]
pub fn matches_all(&self) -> bool {
self.included_prefixes.is_none()
&& self.excluded_prefixes.is_none()
&& self.regular_expression.is_none()
}
#[must_use]
pub fn regular_expression(&self) -> Option<&str> {
self.regular_expression.as_deref()
}
pub fn matches(&self, value: &str, definition_path: &str) -> IndexResult<bool> {
if self.matches_all() {
return Ok(true);
}
if let Some(included) = &self.included_prefixes
&& included.iter().any(|prefix| value.starts_with(prefix))
{
return Ok(true);
}
if let Some(excluded) = &self.excluded_prefixes
&& excluded
.iter()
.any(|prefix| value.starts_with(prefix.as_str()) || prefix.starts_with(value))
{
return Ok(false);
}
match &self.regular_expression {
None if self.included_prefixes.is_some() => Ok(false),
None => Ok(true),
Some(pattern) => Err(IndexError::UnsupportedValuePattern {
definition_path: definition_path.to_owned(),
pattern: pattern.clone(),
}),
}
}
}
fn read_prefixes(
definition: &NodeState<'_>,
property_name: &str,
definition_path: &str,
warnings: &mut Vec<IndexWarning>,
) -> IndexResult<Option<Vec<String>>> {
let Some(property) = definition.property(property_name)? else {
return Ok(None);
};
if is_array(&property) {
note_query_side_disagreement(&property, property_name, warnings);
return Ok(Some(crate::index::converting_strings(Some(&property))));
}
match strict_string(Some(&property)) {
Some(text) => Ok(Some(vec![text.to_owned()])),
None => Err(IndexError::NonStringPrefixValue {
definition_path: definition_path.to_owned(),
property_name: property_name.to_owned(),
}),
}
}
fn is_array(property: &PropertyState) -> bool {
matches!(
property.values,
crate::content::node::PropertyValues::Multiple(_)
)
}
fn note_query_side_disagreement(
property: &PropertyState,
property_name: &str,
warnings: &mut Vec<IndexWarning>,
) {
if strict_strings(Some(property)).is_some() {
return;
}
let stored_type = crate::index::stored_type_name(property);
if property_name == "valueExcludedPrefixes" {
warnings.push(IndexWarning::ExcludedPrefixesIgnoredByQueries { stored_type });
} else {
warnings.push(IndexWarning::IndexedButNeverSelected {
property_name: property_name.to_owned(),
stored_type,
});
}
}
#[cfg(test)]
mod tests {
use super::ValuePattern;
const PATH: &str = "/oak:index/test";
fn prefixes(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_owned()).collect()
}
#[test]
fn an_absent_pattern_admits_everything() {
let pattern = ValuePattern::default();
assert!(pattern.matches_all());
assert!(pattern.matches("anything", PATH).expect("no refusal"));
}
#[test]
fn an_include_prefix_wins_over_an_exclude_prefix() {
let pattern = ValuePattern::new(Some(prefixes(&["ab"])), Some(prefixes(&["a"])), None);
assert!(pattern.matches("abc", PATH).expect("no refusal"));
}
#[test]
fn an_exclude_prefix_matches_in_either_direction() {
let pattern = ValuePattern::new(None, Some(prefixes(&["abc"])), None);
assert!(!pattern.matches("abcdef", PATH).expect("no refusal"));
assert!(!pattern.matches("ab", PATH).expect("no refusal"));
assert!(pattern.matches("b", PATH).expect("no refusal"));
}
#[test]
fn include_prefixes_without_a_pattern_reject_everything_else() {
let pattern = ValuePattern::new(Some(prefixes(&["ab"])), None, None);
assert!(!pattern.matches("xy", PATH).expect("no refusal"));
}
#[test]
fn a_regular_expression_is_refused_only_where_the_rule_reaches_it() {
let pattern = ValuePattern::new(Some(prefixes(&["ab"])), None, Some("x.*".to_owned()));
assert!(
pattern
.matches("abc", PATH)
.expect("the include prefix short-circuits"),
"an include prefix must win before the pattern is consulted"
);
let error = pattern
.matches("xy", PATH)
.expect_err("a value with no include prefix reaches the pattern");
assert!(
matches!(
error,
crate::index::IndexError::UnsupportedValuePattern { .. }
),
"{error}"
);
}
#[test]
fn an_exclude_prefix_answers_before_the_pattern_is_reached() {
let pattern = ValuePattern::new(None, Some(prefixes(&["x"])), Some("x.*".to_owned()));
assert!(!pattern.matches("xy", PATH).expect("the exclusion answers"));
}
}