nl3/lib.rs
1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3//! # nl3
4//!
5//! Natural language triples — parse short plain-English Subject–Predicate–Object
6//! phrases into validated triples.
7//!
8//! You supply a **grammar** (the valid S-P-O relations) and a **vocabulary**
9//! (word-stem → predicate mappings), then [`Nl3::parse`] turns phrases into
10//! [`Triple`]s, validating them and flipping reversed phrasings.
11//!
12//! ```
13//! use nl3::Nl3;
14//!
15//! let nl3 = Nl3::builder()
16//! .grammar(["users message users"])
17//! .vocabulary([("contact", "message"), ("msg", "message")])
18//! .build();
19//!
20//! let triple = nl3.parse("user jack contacts user jill").unwrap();
21//! assert_eq!(triple.subject.ty.as_deref(), Some("user"));
22//! assert_eq!(triple.subject.value.as_deref(), Some("jack"));
23//! assert_eq!(triple.predicate.value.as_deref(), Some("message"));
24//! assert_eq!(triple.object.ty.as_deref(), Some("user"));
25//! assert_eq!(triple.object.value.as_deref(), Some("jill"));
26//! ```
27
28mod classify;
29mod error;
30mod rules;
31pub mod tagger;
32pub mod text;
33mod triple;
34
35use std::collections::HashMap;
36
37pub use error::ParseError;
38pub use tagger::{LexiconTagger, Tagger};
39pub use triple::{Predicate, Term, Triple};
40
41use classify::classify;
42use rules::{Rules, build_triple, process};
43
44/// How to resolve an entity type that the grammar leaves ambiguous.
45///
46/// When a phrase omits a type (e.g. `"jack contacts jill"`), nl3 infers it from
47/// the matched predicate. If the predicate maps to a single type, that type is
48/// always used. This policy only applies when *more than one* candidate type
49/// exists.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum Ambiguity {
52 /// Use the first candidate type, in grammar declaration order. (Default.)
53 #[default]
54 FirstMatch,
55 /// Return [`ParseError::AmbiguousType`] instead of guessing.
56 Error,
57}
58
59/// A configured nl3 client. Create one with [`Nl3::builder`]. Ports the instance
60/// returned by the `create` factory in `index.js`.
61pub struct Nl3 {
62 rules: Rules,
63 tagger: Box<dyn Tagger>,
64 ambiguity: Ambiguity,
65}
66
67impl Nl3 {
68 /// Start building a client.
69 pub fn builder() -> Nl3Builder {
70 Nl3Builder::default()
71 }
72
73 /// Parse a phrase into a [`Triple`]. Ports `lib/parse.js`.
74 ///
75 /// # Errors
76 /// Returns [`ParseError::EmptyInput`] for empty/whitespace input, and
77 /// [`ParseError::InvalidTriple`] when the phrase does not match the grammar
78 /// in either direction.
79 pub fn parse(&self, text: &str) -> Result<Triple, ParseError> {
80 if text.trim().is_empty() {
81 return Err(ParseError::EmptyInput);
82 }
83 let classification = classify(text, self.tagger.as_ref());
84 let triple = build_triple(&classification, &self.rules);
85 process(triple, &classification, &self.rules, self.ambiguity)
86 }
87}
88
89/// Builder for [`Nl3`].
90#[derive(Default)]
91pub struct Nl3Builder {
92 grammar: Vec<String>,
93 vocabulary: HashMap<String, String>,
94 tagger: Option<Box<dyn Tagger>>,
95 ambiguity: Ambiguity,
96}
97
98impl Nl3Builder {
99 /// Set the grammar: valid triples written as `"Subject Predicate Object"`
100 /// (all words are singularized). Replaces any previously set grammar.
101 pub fn grammar<I, S>(mut self, grammar: I) -> Self
102 where
103 I: IntoIterator<Item = S>,
104 S: Into<String>,
105 {
106 self.grammar = grammar.into_iter().map(Into::into).collect();
107 self
108 }
109
110 /// Set the vocabulary: a mapping of word stems to predicates within the
111 /// grammar. Replaces any previously set vocabulary.
112 pub fn vocabulary<I, K, V>(mut self, vocabulary: I) -> Self
113 where
114 I: IntoIterator<Item = (K, V)>,
115 K: Into<String>,
116 V: Into<String>,
117 {
118 self.vocabulary = vocabulary
119 .into_iter()
120 .map(|(k, v)| (k.into(), v.into()))
121 .collect();
122 self
123 }
124
125 /// Override the part-of-speech tagger. Defaults to [`LexiconTagger`].
126 pub fn tagger(mut self, tagger: impl Tagger + 'static) -> Self {
127 self.tagger = Some(Box::new(tagger));
128 self
129 }
130
131 /// Set how ambiguous inferred types are resolved. Defaults to
132 /// [`Ambiguity::FirstMatch`].
133 pub fn ambiguity(mut self, ambiguity: Ambiguity) -> Self {
134 self.ambiguity = ambiguity;
135 self
136 }
137
138 /// Build the client.
139 pub fn build(self) -> Nl3 {
140 Nl3 {
141 rules: Rules::build(&self.grammar, self.vocabulary),
142 tagger: self.tagger.unwrap_or_else(|| Box::new(LexiconTagger)),
143 ambiguity: self.ambiguity,
144 }
145 }
146}