Skip to main content

lindera_binding_core/
tokenizer.rs

1//! Shared tokenizer build-flow orchestration for the bindings.
2//!
3//! Each binding's tokenizer wrapper reimplements the same flow: configure a
4//! [`lindera::tokenizer::TokenizerBuilder`], build a
5//! [`lindera::tokenizer::Tokenizer`], and convert the resulting tokens. This
6//! module collects that orchestration into [`CoreTokenizerBuilder`] and
7//! [`CoreTokenizer`], leaving each binding to do only its FFI-value conversion
8//! (`serde_json::Value` ⇔ the host language's argument type) and a thin wrapper.
9
10use std::path::Path;
11use std::str::FromStr;
12
13use serde_json::Value;
14
15use lindera::dictionary::{Dictionary, UserDictionary};
16use lindera::mode::Mode;
17use lindera::segmenter::Segmenter;
18use lindera::tokenizer::{Tokenizer, TokenizerBuilder};
19
20use crate::error::CoreResult;
21use crate::token::TokenView;
22
23/// Builder that orchestrates tokenizer configuration on behalf of the bindings.
24///
25/// Wraps [`lindera::tokenizer::TokenizerBuilder`]; filter arguments are passed
26/// as [`serde_json::Value`] so the FFI-specific value conversion stays in each
27/// binding.
28pub struct CoreTokenizerBuilder {
29    /// The backing lindera builder.
30    inner: TokenizerBuilder,
31}
32
33impl CoreTokenizerBuilder {
34    /// Creates a builder with the default (empty) configuration.
35    pub fn new() -> CoreResult<Self> {
36        Ok(Self {
37            inner: TokenizerBuilder::new()?,
38        })
39    }
40
41    /// Creates a builder from a YAML configuration file.
42    pub fn from_file(file_path: &Path) -> CoreResult<Self> {
43        Ok(Self {
44            inner: TokenizerBuilder::from_file(file_path)?,
45        })
46    }
47
48    /// Sets the segmenter mode from a string (`"normal"` or `"decompose"`).
49    pub fn set_mode(&mut self, mode: &str) -> CoreResult<&mut Self> {
50        let mode = Mode::from_str(mode)?;
51        self.inner.set_segmenter_mode(&mode);
52        Ok(self)
53    }
54
55    /// Sets the segmenter dictionary URI / path.
56    pub fn set_dictionary(&mut self, uri: &str) -> &mut Self {
57        self.inner.set_segmenter_dictionary(uri);
58        self
59    }
60
61    /// Sets the segmenter user-dictionary URI / path.
62    pub fn set_user_dictionary(&mut self, uri: &str) -> &mut Self {
63        self.inner.set_segmenter_user_dictionary(uri);
64        self
65    }
66
67    /// Sets whether whitespace tokens are kept in the output.
68    pub fn set_keep_whitespace(&mut self, keep_whitespace: bool) -> &mut Self {
69        self.inner.set_segmenter_keep_whitespace(keep_whitespace);
70        self
71    }
72
73    /// Appends a character filter identified by `kind` with JSON `args`.
74    pub fn append_character_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
75        self.inner.append_character_filter(kind, args);
76        self
77    }
78
79    /// Appends a token filter identified by `kind` with JSON `args`.
80    pub fn append_token_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
81        self.inner.append_token_filter(kind, args);
82        self
83    }
84
85    /// Builds a [`CoreTokenizer`] from the current configuration.
86    pub fn build(&self) -> CoreResult<CoreTokenizer> {
87        Ok(CoreTokenizer {
88            inner: self.inner.build()?,
89        })
90    }
91}
92
93/// Tokenizer that orchestrates tokenization on behalf of the bindings.
94///
95/// Wraps [`lindera::tokenizer::Tokenizer`] and returns owned [`TokenView`]s so
96/// the bindings never handle borrowed `lindera` tokens directly.
97pub struct CoreTokenizer {
98    /// The backing lindera tokenizer.
99    inner: Tokenizer,
100}
101
102impl CoreTokenizer {
103    /// Builds a tokenizer from segmenter parts, parsing `mode` from a string.
104    pub fn from_segmenter(
105        mode: &str,
106        dictionary: Dictionary,
107        user_dictionary: Option<UserDictionary>,
108    ) -> CoreResult<Self> {
109        let mode = Mode::from_str(mode)?;
110        let segmenter = Segmenter::new(mode, dictionary, user_dictionary);
111        Ok(Self {
112            inner: Tokenizer::new(segmenter),
113        })
114    }
115
116    /// Wraps an already-built lindera [`Tokenizer`].
117    pub fn from_tokenizer(tokenizer: Tokenizer) -> Self {
118        Self { inner: tokenizer }
119    }
120
121    /// Tokenizes `text`, returning owned [`TokenView`]s.
122    pub fn tokenize(&self, text: &str) -> CoreResult<Vec<TokenView>> {
123        let tokens = self.inner.tokenize(text)?;
124        Ok(tokens.into_iter().map(TokenView::from_token).collect())
125    }
126
127    /// Tokenizes `text` and returns the N-best results as `(tokens, cost)` pairs.
128    pub fn tokenize_nbest(
129        &self,
130        text: &str,
131        n: usize,
132        unique: bool,
133        cost_threshold: Option<i64>,
134    ) -> CoreResult<Vec<(Vec<TokenView>, i64)>> {
135        let results = self.inner.tokenize_nbest(text, n, unique, cost_threshold)?;
136        Ok(results
137            .into_iter()
138            .map(|(tokens, cost)| {
139                (
140                    tokens.into_iter().map(TokenView::from_token).collect(),
141                    cost,
142                )
143            })
144            .collect())
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn builder_new_succeeds() {
154        assert!(CoreTokenizerBuilder::new().is_ok());
155    }
156
157    #[test]
158    fn set_mode_accepts_known_modes() {
159        let mut builder = CoreTokenizerBuilder::new().expect("builder");
160        assert!(builder.set_mode("normal").is_ok());
161        assert!(builder.set_mode("decompose").is_ok());
162    }
163
164    #[test]
165    fn set_mode_rejects_unknown_mode() {
166        let mut builder = CoreTokenizerBuilder::new().expect("builder");
167        assert!(builder.set_mode("definitely-not-a-mode").is_err());
168    }
169
170    #[test]
171    fn infallible_setters_chain() {
172        let mut builder = CoreTokenizerBuilder::new().expect("builder");
173        builder
174            .set_dictionary("embedded://ipadic")
175            .set_keep_whitespace(true)
176            .append_token_filter("japanese_compound_word", &Value::Object(Default::default()));
177        // Reaching here means the borrow-returning setters compose.
178    }
179}