Skip to main content

lindera_nodejs/
metadata.rs

1//! Dictionary metadata configuration.
2//!
3//! This module provides structures for configuring dictionary metadata, including
4//! character encodings and schema definitions. The defaults and schema wiring are
5//! delegated to [`lindera_binding_core::CoreMetadata`]; this module only adds the
6//! napi wrappers.
7
8use std::collections::HashMap;
9
10use lindera::dictionary::Metadata;
11use lindera_binding_core::CoreMetadata;
12
13/// Options for creating a Metadata instance.
14///
15/// All fields are optional. When omitted, default values are used.
16#[napi(object)]
17pub struct MetadataOptions {
18    /// Dictionary name (default: "default").
19    pub name: Option<String>,
20    /// Character encoding (default: "UTF-8").
21    pub encoding: Option<String>,
22    /// Default cost for unknown words (default: -10000).
23    pub default_word_cost: Option<i32>,
24    /// Default left context ID (default: 1288).
25    pub default_left_context_id: Option<u32>,
26    /// Default right context ID (default: 1288).
27    pub default_right_context_id: Option<u32>,
28    /// Default value for missing fields (default: "*").
29    pub default_field_value: Option<String>,
30    /// Allow flexible CSV parsing (default: false).
31    pub flexible_csv: Option<bool>,
32    /// Skip entries with invalid cost or ID (default: false).
33    pub skip_invalid_cost_or_id: Option<bool>,
34    /// Normalize morphological details (default: false).
35    pub normalize_details: Option<bool>,
36}
37
38/// Dictionary metadata configuration.
39///
40/// A thin napi wrapper over [`lindera_binding_core::CoreMetadata`], which owns
41/// the default values and the schema wiring.
42#[napi(js_name = "Metadata")]
43pub struct JsMetadata {
44    /// The backing binding-core metadata.
45    inner: CoreMetadata,
46}
47
48#[napi]
49impl JsMetadata {
50    /// Creates a new Metadata with optional configuration.
51    ///
52    /// # Arguments
53    ///
54    /// * `options` - Optional configuration object. When omitted, all defaults are used.
55    #[napi(constructor)]
56    pub fn new(options: Option<MetadataOptions>) -> Self {
57        let opts = options.unwrap_or(MetadataOptions {
58            name: None,
59            encoding: None,
60            default_word_cost: None,
61            default_left_context_id: None,
62            default_right_context_id: None,
63            default_field_value: None,
64            flexible_csv: None,
65            skip_invalid_cost_or_id: None,
66            normalize_details: None,
67        });
68
69        JsMetadata {
70            inner: CoreMetadata::new(
71                opts.name,
72                opts.encoding,
73                opts.default_word_cost.map(|c| c as i16),
74                opts.default_left_context_id.map(|id| id as u16),
75                opts.default_right_context_id.map(|id| id as u16),
76                opts.default_field_value,
77                opts.flexible_csv,
78                opts.skip_invalid_cost_or_id,
79                opts.normalize_details,
80                None,
81                None,
82            ),
83        }
84    }
85
86    /// Creates a Metadata with all default values.
87    ///
88    /// # Returns
89    ///
90    /// A Metadata instance with default configuration.
91    #[napi(factory)]
92    pub fn create_default() -> Self {
93        JsMetadata::new(None)
94    }
95
96    /// Loads metadata from a JSON file.
97    ///
98    /// # Arguments
99    ///
100    /// * `path` - Path to the JSON metadata file.
101    ///
102    /// # Returns
103    ///
104    /// A Metadata instance loaded from the file.
105    #[napi(factory)]
106    pub fn from_json_file(path: String) -> napi::Result<Self> {
107        let json_str = std::fs::read_to_string(&path).map_err(|e| {
108            napi::Error::new(
109                napi::Status::GenericFailure,
110                format!("Failed to read file: {e}"),
111            )
112        })?;
113
114        let metadata: Metadata = serde_json::from_str(&json_str).map_err(|e| {
115            napi::Error::new(
116                napi::Status::GenericFailure,
117                format!("Failed to parse JSON: {e}"),
118            )
119        })?;
120
121        Ok(metadata.into())
122    }
123
124    /// Dictionary name.
125    #[napi(getter)]
126    pub fn name(&self) -> String {
127        self.inner.name.clone()
128    }
129
130    /// Sets the dictionary name.
131    #[napi(setter)]
132    pub fn set_name(&mut self, name: String) {
133        self.inner.name = name;
134    }
135
136    /// Character encoding.
137    #[napi(getter)]
138    pub fn encoding(&self) -> String {
139        self.inner.encoding.clone()
140    }
141
142    /// Sets the character encoding.
143    #[napi(setter)]
144    pub fn set_encoding(&mut self, encoding: String) {
145        self.inner.encoding = encoding;
146    }
147
148    /// Default word cost.
149    #[napi(getter)]
150    pub fn default_word_cost(&self) -> i32 {
151        self.inner.default_word_cost as i32
152    }
153
154    /// Sets the default word cost.
155    #[napi(setter)]
156    pub fn set_default_word_cost(&mut self, cost: i32) {
157        self.inner.default_word_cost = cost as i16;
158    }
159
160    /// Default left context ID.
161    #[napi(getter)]
162    pub fn default_left_context_id(&self) -> u32 {
163        self.inner.default_left_context_id as u32
164    }
165
166    /// Sets the default left context ID.
167    #[napi(setter)]
168    pub fn set_default_left_context_id(&mut self, id: u32) {
169        self.inner.default_left_context_id = id as u16;
170    }
171
172    /// Default right context ID.
173    #[napi(getter)]
174    pub fn default_right_context_id(&self) -> u32 {
175        self.inner.default_right_context_id as u32
176    }
177
178    /// Sets the default right context ID.
179    #[napi(setter)]
180    pub fn set_default_right_context_id(&mut self, id: u32) {
181        self.inner.default_right_context_id = id as u16;
182    }
183
184    /// Default field value for missing fields.
185    #[napi(getter)]
186    pub fn default_field_value(&self) -> String {
187        self.inner.default_field_value.clone()
188    }
189
190    /// Sets the default field value.
191    #[napi(setter)]
192    pub fn set_default_field_value(&mut self, value: String) {
193        self.inner.default_field_value = value;
194    }
195
196    /// Whether flexible CSV parsing is enabled.
197    #[napi(getter)]
198    pub fn flexible_csv(&self) -> bool {
199        self.inner.flexible_csv
200    }
201
202    /// Sets flexible CSV parsing.
203    #[napi(setter)]
204    pub fn set_flexible_csv(&mut self, value: bool) {
205        self.inner.flexible_csv = value;
206    }
207
208    /// Whether to skip entries with invalid cost or ID.
209    #[napi(getter)]
210    pub fn skip_invalid_cost_or_id(&self) -> bool {
211        self.inner.skip_invalid_cost_or_id
212    }
213
214    /// Sets whether to skip invalid entries.
215    #[napi(setter)]
216    pub fn set_skip_invalid_cost_or_id(&mut self, value: bool) {
217        self.inner.skip_invalid_cost_or_id = value;
218    }
219
220    /// Whether to normalize morphological details.
221    #[napi(getter)]
222    pub fn normalize_details(&self) -> bool {
223        self.inner.normalize_details
224    }
225
226    /// Sets whether to normalize details.
227    #[napi(setter)]
228    pub fn set_normalize_details(&mut self, value: bool) {
229        self.inner.normalize_details = value;
230    }
231
232    /// Returns a plain object representation of the metadata.
233    ///
234    /// # Returns
235    ///
236    /// A HashMap containing all metadata properties as strings.
237    #[napi]
238    pub fn to_object(&self) -> HashMap<String, String> {
239        let mut dict = HashMap::new();
240        dict.insert("name".to_string(), self.inner.name.clone());
241        dict.insert("encoding".to_string(), self.inner.encoding.clone());
242        dict.insert(
243            "defaultWordCost".to_string(),
244            self.inner.default_word_cost.to_string(),
245        );
246        dict.insert(
247            "defaultLeftContextId".to_string(),
248            self.inner.default_left_context_id.to_string(),
249        );
250        dict.insert(
251            "defaultRightContextId".to_string(),
252            self.inner.default_right_context_id.to_string(),
253        );
254        dict.insert(
255            "defaultFieldValue".to_string(),
256            self.inner.default_field_value.clone(),
257        );
258        dict.insert(
259            "flexibleCsv".to_string(),
260            self.inner.flexible_csv.to_string(),
261        );
262        dict.insert(
263            "skipInvalidCostOrId".to_string(),
264            self.inner.skip_invalid_cost_or_id.to_string(),
265        );
266        dict.insert(
267            "normalizeDetails".to_string(),
268            self.inner.normalize_details.to_string(),
269        );
270        dict
271    }
272}
273
274impl JsMetadata {
275    /// Converts a reference to JsMetadata into a lindera Metadata.
276    ///
277    /// # Arguments
278    ///
279    /// * `metadata` - Reference to JsMetadata.
280    ///
281    /// # Returns
282    ///
283    /// A lindera Metadata instance.
284    pub fn to_lindera_metadata(metadata: &JsMetadata) -> Metadata {
285        metadata.inner.clone().into()
286    }
287}
288
289impl From<JsMetadata> for Metadata {
290    fn from(metadata: JsMetadata) -> Self {
291        metadata.inner.into()
292    }
293}
294
295impl From<Metadata> for JsMetadata {
296    fn from(metadata: Metadata) -> Self {
297        JsMetadata {
298            inner: CoreMetadata::from(metadata),
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_js_metadata_to_lindera_metadata() {
309        let js_metadata = JsMetadata::new(None);
310        let lindera_metadata: Metadata = js_metadata.into();
311        assert_eq!(lindera_metadata.name, "default");
312        assert_eq!(lindera_metadata.encoding, "UTF-8");
313        assert_eq!(lindera_metadata.default_word_cost, -10000);
314        assert_eq!(lindera_metadata.default_left_context_id, 1288);
315        assert_eq!(lindera_metadata.default_right_context_id, 1288);
316        assert_eq!(lindera_metadata.default_field_value, "*");
317        assert!(!lindera_metadata.flexible_csv);
318        assert!(!lindera_metadata.skip_invalid_cost_or_id);
319        assert!(!lindera_metadata.normalize_details);
320    }
321
322    #[test]
323    fn test_lindera_metadata_to_js_metadata() {
324        let lindera_metadata = Metadata::new(
325            "test".to_string(),
326            "EUC-JP".to_string(),
327            -5000,
328            100,
329            200,
330            "-".to_string(),
331            true,
332            true,
333            true,
334            lindera::dictionary::Schema::default(),
335            lindera::dictionary::Schema::default(),
336        );
337        let js_metadata: JsMetadata = lindera_metadata.into();
338        assert_eq!(js_metadata.name(), "test");
339        assert_eq!(js_metadata.encoding(), "EUC-JP");
340        assert_eq!(js_metadata.default_word_cost(), -5000);
341        assert_eq!(js_metadata.default_left_context_id(), 100);
342        assert_eq!(js_metadata.default_right_context_id(), 200);
343        assert_eq!(js_metadata.default_field_value(), "-");
344        assert!(js_metadata.flexible_csv());
345        assert!(js_metadata.skip_invalid_cost_or_id());
346        assert!(js_metadata.normalize_details());
347    }
348
349    #[test]
350    fn test_js_metadata_with_custom_options() {
351        let opts = MetadataOptions {
352            name: Some("custom".to_string()),
353            encoding: Some("Shift_JIS".to_string()),
354            default_word_cost: Some(-5000),
355            default_left_context_id: Some(100),
356            default_right_context_id: Some(200),
357            default_field_value: Some("-".to_string()),
358            flexible_csv: Some(true),
359            skip_invalid_cost_or_id: Some(true),
360            normalize_details: Some(true),
361        };
362        let js_metadata = JsMetadata::new(Some(opts));
363        assert_eq!(js_metadata.name(), "custom");
364        assert_eq!(js_metadata.encoding(), "Shift_JIS");
365        assert_eq!(js_metadata.default_word_cost(), -5000);
366        assert_eq!(js_metadata.default_left_context_id(), 100);
367        assert_eq!(js_metadata.default_right_context_id(), 200);
368        assert_eq!(js_metadata.default_field_value(), "-");
369        assert!(js_metadata.flexible_csv());
370        assert!(js_metadata.skip_invalid_cost_or_id());
371        assert!(js_metadata.normalize_details());
372    }
373
374    #[test]
375    fn test_js_metadata_roundtrip() {
376        let original = JsMetadata::new(None);
377        let lindera: Metadata = original.into();
378        let roundtripped: JsMetadata = lindera.into();
379        assert_eq!(roundtripped.name(), "default");
380        assert_eq!(roundtripped.encoding(), "UTF-8");
381        assert_eq!(roundtripped.default_word_cost(), -10000);
382        assert_eq!(roundtripped.default_left_context_id(), 1288);
383        assert_eq!(roundtripped.default_right_context_id(), 1288);
384    }
385}