use crate::schema::{ArchivedDictionary, ArchivedEntry, Dictionary, Entry};
use rayon::prelude::*;
use rkyv::option::ArchivedOption;
use std::marker::{Send, Sync};
#[derive(Debug, PartialEq, Clone)]
pub enum LookupStrategy {
Exact,
Split(usize),
}
#[derive(Debug, Clone)]
pub struct LookupOptions {
pub follow: bool,
pub strategy: LookupStrategy,
pub insensitive: bool,
}
impl AsRef<LookupOptions> for LookupOptions {
fn as_ref(&self) -> &Self {
self
}
}
impl Default for LookupOptions {
fn default() -> Self {
Self {
follow: false,
strategy: LookupStrategy::Exact,
insensitive: false,
}
}
}
impl LookupOptions {
pub fn follow(mut self, follow: bool) -> Self {
self.follow = follow;
self
}
pub fn strategy(mut self, strategy: LookupStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn insensitive(mut self, insensitive: bool) -> Self {
self.insensitive = insensitive;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LookupResult<E> {
pub entry: E,
pub directed_from: Option<E>,
}
macro_rules! lookup {
($tys:ident, $ret:ident, $opt:ident) => {
impl $tys {
#[doc = r#"Attempt to find a single entry by term.
This helper supports optional redirect following and an optional
case-insensitive retry (lowercasing the query) when configured.
Returns Some(LookupResult) on a match, or None if not found."#]
fn find_entry<'a>(
&'a self,
follow: &bool,
insensitive: &bool,
query: &str,
directed_from: Option<&'a $ret>,
path: &mut Vec<String>,
) -> crate::Result<$opt<LookupResult<&'a $ret>>> {
if path.contains(&query.to_string()) {
let mut chain = path.clone();
chain.push(query.to_string());
return Err(crate::Error::RedirectLoop(chain.join(" -> ")));
}
path.push(query.to_string());
if let Some(entry) = self.entries.get(query) {
if *follow && entry.etymologies.is_empty() {
if let Option::Some(also) = &entry.see_also.as_ref() {
if also.len() > 0 {
let result = self.find_entry(
follow,
insensitive,
also,
directed_from.or(Some(entry)),
path,
);
path.pop();
return result;
}
}
}
path.pop();
return Ok($opt::Some(LookupResult {
entry,
directed_from,
}));
}
if *insensitive {
let query_lower = query.to_lowercase();
if query_lower != query {
if let Ok($opt::Some(result)) =
self.find_entry(follow, insensitive, &query_lower, directed_from, path)
{
path.pop();
return Ok($opt::Some(result));
}
}
}
path.pop();
Ok($opt::None)
}
fn perform_split<'a>(
&'a self,
query: &str,
options: &crate::split::SplitOptions,
) -> crate::Result<Vec<LookupResult<&'a $ret>>> {
let crate::split::SplitOptions {
threshold,
follow,
insensitive,
} = options;
let chars: Vec<_> = query.chars().collect();
let mut results: Vec<LookupResult<&'a $ret>> = Vec::new();
let mut start = 0;
let mut end = chars.len();
while start < end {
let substr: String = chars[start..end].iter().collect();
let mut path = Vec::new();
match self.find_entry(follow, insensitive, substr.as_str(), None, &mut path) {
Ok($opt::Some(result)) => {
results.push(result);
start = end;
end = chars.len();
}
Ok($opt::None) => {
if end - start <= *threshold {
start = end;
end = chars.len();
} else {
end -= 1;
}
}
Err(e) => return Err(e),
}
}
Ok(results)
}
#[doc = r#"Perform lookup for a single query using the provided options.
Depending on the strategy, this may return zero or more results."#]
fn perform_lookup<'a, Options>(
&'a self,
query: &str,
options: Options,
) -> crate::Result<Vec<LookupResult<&'a $ret>>>
where
Options: AsRef<LookupOptions> + Send + Sync,
{
let LookupOptions {
follow,
strategy,
insensitive,
} = options.as_ref();
let mut path = Vec::new();
if let $opt::Some(result) =
self.find_entry(follow, insensitive, query, None, &mut path)?
{
return Ok(vec![result]);
}
if let LookupStrategy::Split(min_length) = strategy {
let split_opts = crate::split::SplitOptions::default()
.threshold(*min_length)
.follow(*follow)
.insensitive(*insensitive);
return self.perform_split(query, &split_opts);
}
Ok(vec![])
}
pub fn split<'a, Query, Options>(
&'a self,
queries: &Vec<Query>,
options: Options,
) -> crate::Result<Vec<LookupResult<&'a $ret>>>
where
Query: AsRef<str> + Send + Sync,
Options: AsRef<crate::split::SplitOptions> + Send + Sync,
{
queries
.par_iter()
.map(|q| self.perform_split(q.as_ref(), options.as_ref()))
.collect::<crate::Result<Vec<_>>>()
.map(|v| v.into_iter().flatten().collect())
}
#[doc = r#"Lookup multiple queries in parallel.
Each query is processed independently with the provided options.
Returns all matches without a guaranteed order.
Examples
--------
```rust
use odict::{OpenDictionary, LookupOptions, LookupStrategy};
# fn demo(dict: &odict::OpenDictionary) -> odict::Result<()> {
let archived = dict.contents()?;
let queries = vec!["hello", "world"];
let options = LookupOptions::default()
.insensitive(true)
.strategy(LookupStrategy::Exact);
let results = archived.lookup(&queries, options)?;
# Ok(())
# }
```"#]
pub fn lookup<'a, 'b, Query, Options>(
&'a self,
queries: &'b Vec<Query>,
options: Options,
) -> crate::Result<Vec<LookupResult<&'a $ret>>>
where
Query: AsRef<str> + Send + Sync,
Options: AsRef<LookupOptions> + Send + Sync,
{
let results = queries
.par_iter()
.map(|query| self.perform_lookup(query.as_ref(), &options))
.collect::<crate::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<LookupResult<&'a $ret>>>();
Ok(results)
}
}
};
}
lookup!(Dictionary, Entry, Option);
lookup!(ArchivedDictionary, ArchivedEntry, ArchivedOption);
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{lookup::LookupOptions, schema::Dictionary};
#[test]
fn test_lookup_follow_limit() {
let xml = r#"
<dictionary>
<entry term="target">
<ety>
<sense pos="n">
<definition value="The final destination" />
</sense>
</ety>
</entry>
<entry term="alias2" see="target" />
<entry term="alias1" see="alias2" />
</dictionary>
"#;
let dict = Dictionary::from_str(xml).unwrap();
let result = dict
.lookup(&vec!["alias1"], LookupOptions::default().follow(false))
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "alias1");
assert!(result[0].directed_from.is_none());
let result = dict
.lookup(&vec!["alias1"], LookupOptions::default().follow(true))
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "target");
assert!(result[0].directed_from.is_some());
assert_eq!(result[0].directed_from.unwrap().term, "alias1");
let result = dict
.lookup(&vec!["alias2"], LookupOptions::default().follow(true))
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "target");
assert!(result[0].directed_from.is_some());
assert_eq!(result[0].directed_from.unwrap().term, "alias2");
}
#[test]
fn test_lookup_redirect_loop_detection() {
let xml = r#"
<dictionary>
<entry term="loop1" see="loop2" />
<entry term="loop2" see="loop1" />
</dictionary>
"#;
let dict = Dictionary::from_str(xml).unwrap();
let result = dict.lookup(&vec!["loop1"], LookupOptions::default().follow(true));
assert!(result.is_err());
let error_message = result.unwrap_err().to_string();
assert_eq!(
error_message,
"Redirect loop detected: loop1 -> loop2 -> loop1"
);
assert!(error_message.contains("loop1"));
assert!(error_message.contains("loop2"));
}
#[test]
fn test_lookup_redirect_case_insensitive() {
let xml = r#"
<dictionary>
<entry term="target">
<ety>
<sense pos="n">
<definition value="The final destination" />
</sense>
</ety>
</entry>
<entry term="alias" see="target" />
</dictionary>
"#;
let dict = Dictionary::from_str(xml).unwrap();
let result = dict
.lookup(
&vec!["ALIAS"],
LookupOptions::default().follow(true).insensitive(true),
)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "target");
assert!(result[0].directed_from.is_some());
assert_eq!(result[0].directed_from.unwrap().term, "alias");
let result = dict
.lookup(
&vec!["Alias"],
LookupOptions::default().follow(true).insensitive(true),
)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "target");
assert!(result[0].directed_from.is_some());
assert_eq!(result[0].directed_from.unwrap().term, "alias");
let result = dict
.lookup(
&vec!["ALIAS"],
LookupOptions::default().follow(true).insensitive(false),
)
.unwrap();
assert_eq!(result.len(), 0);
let result = dict
.lookup(
&vec!["alias"],
LookupOptions::default().follow(true).insensitive(false),
)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].entry.term, "target");
}
}