#![deny(missing_docs)]
use indexmap::IndexMap;
#[cfg(feature = "rhai")]
pub use rhai::Engine;
#[cfg(feature = "rhai")]
use rhai::{Array, Scope, AST};
use std::cmp::Ordering;
#[cfg(feature = "strsim")]
use strsim::{self};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Predicate {
pub code: String,
pub remaining_code: String,
pub texts: Vec<String>,
pub can_commit: bool,
}
pub struct Translator {
dictionary: IndexMap<String, Vec<String>>,
#[cfg(feature = "rhai")]
translators: IndexMap<String, AST>,
auto_commit: bool,
}
impl Translator {
pub fn new(dictionary: IndexMap<String, Vec<String>>, auto_commit: bool) -> Self {
Self {
dictionary,
auto_commit,
#[cfg(feature = "rhai")]
translators: IndexMap::default(),
}
}
#[cfg(feature = "rhai")]
pub fn register(&mut self, name: String, ast: AST) {
self.translators.insert(name, ast);
}
#[cfg(feature = "rhai")]
pub fn unregister(&mut self, name: &str) {
self.translators.shift_remove(name);
}
pub fn translate(&self, input: &str) -> Vec<Predicate> {
#[cfg(feature = "rhai")]
let mut scope = Scope::new();
#[cfg(feature = "rhai")]
let engine = Engine::new();
let predicates = self.dictionary.iter().filter_map(|(key, values)| {
if input.len() < 2 || input.len() > key.len() || key[0..1] != input[0..1] {
return None;
};
let predicate = (key == input).then_some((
1.0,
Predicate {
code: key.to_owned(),
remaining_code: "".to_owned(),
texts: values.to_owned(),
can_commit: self.auto_commit,
},
));
#[cfg(feature = "strsim")]
let predicate = predicate.or_else(|| {
if key.len() == input.len() {
let confidence = strsim::hamming(key.as_ref(), input)
.map(|n| 1.0 - (n as f64 / key.len() as f64))
.unwrap_or(0.0);
(confidence > 0.7).then(|| {
(
confidence,
Predicate {
code: key.to_owned(),
remaining_code: "".to_owned(),
texts: values.to_owned(),
can_commit: false,
},
)
})
} else {
None
}
});
predicate.or_else(|| {
key.starts_with(input).then_some((
0.5,
Predicate {
code: key.to_owned(),
remaining_code: key.chars().skip(input.len()).collect(),
texts: values.to_owned(),
can_commit: false,
},
))
})
});
#[cfg(feature = "rhai")]
let predicates =
predicates.chain(self.translators.iter().filter_map(|(_name, translator)| {
let mut data = engine
.call_fn::<Array>(&mut scope, translator, "translate", (input.to_owned(),))
.unwrap_or_default();
(data.len() == 4).then(|| {
let code = data.remove(0).into_string().unwrap();
let remaining_code = data.remove(0).into_string().unwrap();
let value = data.remove(0);
let values = if value.is_array() {
value.into_array().unwrap()
} else {
vec![value]
};
let values = values
.into_iter()
.map(|e| e.into_string().unwrap())
.collect();
let translated = data.remove(0).as_bool().unwrap();
(
1.0,
Predicate {
code,
remaining_code,
texts: values,
can_commit: translated,
},
)
})
}));
let mut predicates = predicates.collect::<Vec<(f64, Predicate)>>();
predicates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
predicates
.into_iter()
.map(|(_, predicate)| predicate)
.collect()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_translate() {
#[cfg(feature = "rhai")]
use crate::Engine;
use crate::{Predicate, Translator};
use indexmap::IndexMap;
let mut dictionary = IndexMap::new();
dictionary.insert("halo".to_string(), ["hello".to_string()].to_vec());
#[cfg(not(feature = "rhai"))]
let translator = Translator::new(dictionary, true);
#[cfg(feature = "rhai")]
let mut translator = Translator::new(dictionary, true);
#[cfg(feature = "rhai")]
{
let engine = Engine::new();
let ast1 = engine.compile("fn translate(input) {}").unwrap();
let ast2 = engine
.compile(
r#"
fn translate(input) {
if input == "hi" {
["hi", "", "hello", true]
}
}
"#,
)
.unwrap();
translator.register("none".to_string(), ast1);
translator.unregister("none");
translator.register("some".to_string(), ast2);
}
assert_eq!(translator.translate("h"), vec![]);
#[cfg(feature = "rhai")]
assert_eq!(
translator.translate("hi"),
vec![Predicate {
code: "hi".to_owned(),
remaining_code: "".to_owned(),
texts: vec!["hello".to_owned()],
can_commit: true
}]
);
assert_eq!(
translator.translate("ha"),
vec![Predicate {
code: "halo".to_owned(),
remaining_code: "lo".to_owned(),
texts: vec!["hello".to_owned()],
can_commit: false
}]
);
#[cfg(feature = "strsim")]
assert_eq!(
translator.translate("helo"),
vec![Predicate {
code: "halo".to_owned(),
remaining_code: "".to_owned(),
texts: vec!["hello".to_owned()],
can_commit: false
}]
);
}
}