use std::{
borrow::Cow,
ops::ControlFlow,
};
use crate::policy::FieldNameMatching;
#[must_use]
pub(crate) fn canonicalize_field_name(name: &str) -> Cow<'_, str> {
let name = name.trim();
let already_canonical = name.chars().all(|ch| {
if is_field_separator(ch) {
return false;
}
let mut lowercase = ch.to_lowercase();
lowercase.next() == Some(ch) && lowercase.next().is_none()
});
if already_canonical {
return Cow::Borrowed(name);
}
let mut canonical = String::with_capacity(name.len());
canonical.extend(
name.chars()
.filter(|ch| !is_field_separator(*ch))
.flat_map(char::to_lowercase),
);
Cow::Owned(canonical)
}
pub(crate) fn visit_canonical_field_candidates<B>(
name: &str,
matching: FieldNameMatching,
mut visitor: impl FnMut(bool, &str) -> ControlFlow<B>,
) -> ControlFlow<B> {
let canonical = canonicalize_field_name(name);
visitor(true, &canonical)?;
if matching == FieldNameMatching::ExactOrTokenSuffix {
visit_token_suffixes(name, &canonical, &mut visitor)?;
}
ControlFlow::Continue(())
}
fn visit_token_suffixes<B>(
name: &str,
canonical: &str,
visitor: &mut impl FnMut(bool, &str) -> ControlFlow<B>,
) -> ControlFlow<B> {
let mut canonical_offset = 0;
let mut previous = None;
let mut in_token = false;
let mut chars = name.trim().chars().peekable();
while let Some(ch) = chars.next() {
if is_field_separator(ch) {
in_token = false;
previous = Some(ch);
continue;
}
if (!in_token
|| starts_camel_token(previous, ch, chars.peek().copied()))
&& canonical_offset > 0
{
let candidate = &canonical[canonical_offset..];
visitor(false, candidate)?;
}
canonical_offset +=
ch.to_lowercase().map(char::len_utf8).sum::<usize>();
in_token = true;
previous = Some(ch);
}
ControlFlow::Continue(())
}
#[must_use]
#[inline]
fn is_field_separator(ch: char) -> bool {
matches!(ch, '_' | '-' | '.' | '[' | ']') || ch.is_whitespace()
}
#[must_use]
#[inline]
fn starts_camel_token(
previous: Option<char>,
current: char,
next: Option<char>,
) -> bool {
if !current.is_uppercase() {
return false;
}
let follows_lower_or_number = previous.is_some_and(|previous| {
previous.is_lowercase() || previous.is_numeric()
});
let starts_word_after_acronym = previous.is_some_and(char::is_uppercase)
&& next.is_some_and(char::is_lowercase);
follows_lower_or_number || starts_word_after_acronym
}