use std::borrow::Cow;
use std::collections::BTreeMap;
use crate::scope::IndexScope;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DocumentText {
fields: Vec<(String, String)>,
}
impl DocumentText {
pub fn from_fields<I: IntoIterator<Item = (String, String)>>(fields: I) -> Self {
let sorted: BTreeMap<String, String> = fields.into_iter().collect();
Self {
fields: sorted.into_iter().collect(),
}
}
pub fn fields(&self) -> &[(String, String)] {
&self.fields
}
pub fn into_fields(self) -> Vec<(String, String)> {
self.fields
}
pub fn is_empty(&self) -> bool {
self.fields.iter().all(|(_, t)| t.is_empty())
}
pub fn whole(&self) -> String {
let mut out = String::new();
for (i, (_, text)) in self.fields.iter().enumerate() {
if i != 0 {
out.push(' ');
}
out.push_str(text);
}
out
}
pub fn field_scopes<'s>(
&'s self,
collection: &'s str,
) -> impl Iterator<Item = (IndexScope<'s>, &'s str)> {
self.fields
.iter()
.filter_map(move |(f, t)| IndexScope::field(collection, f).map(|s| (s, t.as_str())))
}
pub fn text_of(&self, index: IndexScope<'_>) -> Cow<'_, str> {
match index.field_name() {
None => Cow::Owned(self.whole()),
Some(f) => Cow::Borrowed(
self.fields
.iter()
.find(|(n, _)| n == f)
.map_or("", |(_, t)| t.as_str()),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pairs() -> Vec<(String, String)> {
vec![
("title".into(), "Rust book".into()),
("body".into(), "fearless concurrency".into()),
("author".into(), "".into()),
("zeta".into(), "last".into()),
]
}
fn reference_join(mut texts: Vec<(String, String)>) -> String {
texts.sort_by(|a, b| a.0.cmp(&b.0));
texts
.into_iter()
.map(|(_, t)| t)
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn whole_matches_the_field_name_ordered_join() {
let text = DocumentText::from_fields(pairs());
assert_eq!(text.whole(), reference_join(pairs()));
assert_eq!(text.whole(), " fearless concurrency Rust book last");
}
#[test]
fn repeated_name_keeps_last_text() {
let text = DocumentText::from_fields([
("a".to_string(), "one".to_string()),
("a".to_string(), "two".to_string()),
]);
assert_eq!(text.fields(), &[("a".to_string(), "two".to_string())]);
}
#[test]
fn text_of_resolves_document_and_field_scopes() {
let text = DocumentText::from_fields(pairs());
let title = IndexScope::field("c", "title").expect("non-empty field");
let missing = IndexScope::field("c", "missing").expect("non-empty field");
assert_eq!(text.text_of(title), "Rust book");
assert_eq!(text.text_of(missing), "");
assert_eq!(text.text_of(IndexScope::document("c")), text.whole());
}
#[test]
fn field_scopes_skip_empty_names() {
let text = DocumentText::from_fields([
(String::new(), "orphan".to_string()),
("title".to_string(), "kept".to_string()),
]);
let scopes: Vec<_> = text.field_scopes("c").collect();
assert_eq!(scopes.len(), 1);
assert_eq!(scopes[0].0.field_name(), Some("title"));
assert_eq!(scopes[0].1, "kept");
}
#[test]
fn empty_when_no_field_has_text() {
assert!(DocumentText::default().is_empty());
assert!(DocumentText::from_fields([("a".to_string(), String::new())]).is_empty());
assert!(!DocumentText::from_fields(pairs()).is_empty());
}
}