use super::filter;
use crate::constants::{
K_VOCABULARY_MAX_KEYS, K_VOCABULARY_MAX_SUGGESTIONS, K_VOCABULARY_SAMPLE_ELEMENTS,
VOCABULARY_SUGGESTION_MIN_SIMILARITY,
};
use serde_json::Value;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyOrigin {
Element,
EnvelopeOnly,
Absent,
}
pub struct Scope<'a> {
elements: &'a [Value],
envelope: &'a Value,
}
impl<'a> Scope<'a> {
pub fn new(elements: &'a [Value], envelope: &'a Value) -> Self {
Self { elements, envelope }
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
pub fn classify(&self, key: &str) -> KeyOrigin {
if self
.elements
.iter()
.any(|element| filter::resolve(element, key).is_some())
{
return KeyOrigin::Element;
}
if filter::resolve(self.envelope, key).is_some() {
return KeyOrigin::EnvelopeOnly;
}
KeyOrigin::Absent
}
pub fn suggestions(&self, key: &str) -> Vec<String> {
let (vocabulary, _) = self.candidate_keys();
if vocabulary.is_empty() {
return Vec::new();
}
let comparator = rapidfuzz::distance::jaro_winkler::BatchComparator::new(key.chars());
let mut ranked: Vec<(f64, &str)> = vocabulary
.iter()
.map(|candidate| {
(
comparator.normalized_similarity(candidate.chars()),
*candidate,
)
})
.filter(|(score, _)| *score >= VOCABULARY_SUGGESTION_MIN_SIMILARITY)
.collect();
ranked.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.1.cmp(b.1))
});
ranked.truncate(K_VOCABULARY_MAX_SUGGESTIONS);
ranked
.into_iter()
.map(|(_, name)| name.to_string())
.collect()
}
pub fn vocabulary_is_partial(&self) -> bool {
self.elements.len() > K_VOCABULARY_SAMPLE_ELEMENTS || self.candidate_keys().1
}
fn candidate_keys(&self) -> (BTreeSet<&'a str>, bool) {
let mut names = BTreeSet::new();
if self.elements.is_empty() {
let capped = self
.envelope
.as_object()
.is_some_and(|map| Self::absorb(map.keys(), &mut names));
return (names, capped);
}
for element in self.elements.iter().take(K_VOCABULARY_SAMPLE_ELEMENTS) {
let Some(map) = element.as_object() else {
continue;
};
if Self::absorb(map.keys(), &mut names) {
return (names, true);
}
}
(names, false)
}
fn absorb<I>(keys: I, names: &mut BTreeSet<&'a str>) -> bool
where
I: Iterator<Item = &'a String>,
{
for name in keys {
if names.len() >= K_VOCABULARY_MAX_KEYS {
return true;
}
names.insert(name.as_str());
}
false
}
}