Skip to main content

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}