Skip to main content

lindera_ruby/
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//! magnus wrappers.
7
8use std::collections::HashMap;
9
10use magnus::prelude::*;
11use magnus::{Error, Ruby, function, method};
12
13use lindera::dictionary::Metadata;
14use lindera_binding_core::CoreMetadata;
15
16/// Dictionary metadata configuration.
17///
18/// A thin magnus wrapper over [`lindera_binding_core::CoreMetadata`], which owns
19/// the default values and the schema wiring.
20#[magnus::wrap(class = "Lindera::Metadata", free_immediately, size)]
21#[derive(Debug, Clone)]
22pub struct RbMetadata {
23    /// The backing binding-core metadata.
24    inner: CoreMetadata,
25}
26
27impl RbMetadata {
28    /// Creates a new `RbMetadata` with optional parameters.
29    ///
30    /// # Arguments
31    ///
32    /// All arguments are optional. Default values are used if not provided.
33    ///
34    /// # Returns
35    ///
36    /// A new `RbMetadata` instance.
37    #[allow(clippy::too_many_arguments)]
38    fn new(
39        name: Option<String>,
40        encoding: Option<String>,
41        default_word_cost: Option<i16>,
42        default_left_context_id: Option<u16>,
43        default_right_context_id: Option<u16>,
44        default_field_value: Option<String>,
45        flexible_csv: Option<bool>,
46        skip_invalid_cost_or_id: Option<bool>,
47        normalize_details: Option<bool>,
48    ) -> Self {
49        RbMetadata {
50            inner: CoreMetadata::new(
51                name,
52                encoding,
53                default_word_cost,
54                default_left_context_id,
55                default_right_context_id,
56                default_field_value,
57                flexible_csv,
58                skip_invalid_cost_or_id,
59                normalize_details,
60                None,
61                None,
62            ),
63        }
64    }
65
66    /// Creates a default metadata instance.
67    ///
68    /// # Returns
69    ///
70    /// A new `RbMetadata` with default values.
71    fn create_default() -> Self {
72        RbMetadata::new(None, None, None, None, None, None, None, None, None)
73    }
74
75    /// Loads metadata from a JSON file.
76    ///
77    /// # Arguments
78    ///
79    /// * `path` - Path to the JSON file.
80    ///
81    /// # Returns
82    ///
83    /// A new `RbMetadata` loaded from the file.
84    fn from_json_file(path: String) -> Result<Self, Error> {
85        let ruby = Ruby::get().expect("Ruby runtime not initialized");
86
87        let json_str = std::fs::read_to_string(&path).map_err(|e| {
88            Error::new(
89                ruby.exception_io_error(),
90                format!("Failed to read file: {e}"),
91            )
92        })?;
93
94        let metadata: Metadata = serde_json::from_str(&json_str).map_err(|e| {
95            Error::new(
96                ruby.exception_arg_error(),
97                format!("Failed to parse JSON: {e}"),
98            )
99        })?;
100
101        Ok(metadata.into())
102    }
103
104    /// Returns the dictionary name.
105    fn name(&self) -> String {
106        self.inner.name.clone()
107    }
108
109    /// Returns the character encoding.
110    fn encoding(&self) -> String {
111        self.inner.encoding.clone()
112    }
113
114    /// Returns the default word cost.
115    fn default_word_cost(&self) -> i16 {
116        self.inner.default_word_cost
117    }
118
119    /// Returns the default left context ID.
120    fn default_left_context_id(&self) -> u16 {
121        self.inner.default_left_context_id
122    }
123
124    /// Returns the default right context ID.
125    fn default_right_context_id(&self) -> u16 {
126        self.inner.default_right_context_id
127    }
128
129    /// Returns the default field value.
130    fn default_field_value(&self) -> String {
131        self.inner.default_field_value.clone()
132    }
133
134    /// Returns whether flexible CSV parsing is enabled.
135    fn flexible_csv(&self) -> bool {
136        self.inner.flexible_csv
137    }
138
139    /// Returns whether invalid cost/ID entries should be skipped.
140    fn skip_invalid_cost_or_id(&self) -> bool {
141        self.inner.skip_invalid_cost_or_id
142    }
143
144    /// Returns whether morphological details should be normalized.
145    fn normalize_details(&self) -> bool {
146        self.inner.normalize_details
147    }
148
149    /// Converts the metadata to a Ruby hash.
150    ///
151    /// # Returns
152    ///
153    /// A HashMap of metadata properties.
154    fn to_hash(&self) -> HashMap<String, String> {
155        let mut dict = HashMap::new();
156        dict.insert("name".to_string(), self.inner.name.clone());
157        dict.insert("encoding".to_string(), self.inner.encoding.clone());
158        dict.insert(
159            "default_word_cost".to_string(),
160            self.inner.default_word_cost.to_string(),
161        );
162        dict.insert(
163            "default_left_context_id".to_string(),
164            self.inner.default_left_context_id.to_string(),
165        );
166        dict.insert(
167            "default_right_context_id".to_string(),
168            self.inner.default_right_context_id.to_string(),
169        );
170        dict.insert(
171            "default_field_value".to_string(),
172            self.inner.default_field_value.clone(),
173        );
174        dict.insert(
175            "flexible_csv".to_string(),
176            self.inner.flexible_csv.to_string(),
177        );
178        dict.insert(
179            "skip_invalid_cost_or_id".to_string(),
180            self.inner.skip_invalid_cost_or_id.to_string(),
181        );
182        dict.insert(
183            "normalize_details".to_string(),
184            self.inner.normalize_details.to_string(),
185        );
186        dict.insert(
187            "dictionary_schema_fields".to_string(),
188            self.inner.dictionary_schema.fields().join(","),
189        );
190        dict.insert(
191            "user_dictionary_schema_fields".to_string(),
192            self.inner.user_dictionary_schema.fields().join(","),
193        );
194        dict
195    }
196
197    /// Returns the string representation of the metadata.
198    fn to_s(&self) -> String {
199        format!(
200            "Metadata(name='{}', encoding='{}')",
201            self.inner.name, self.inner.encoding,
202        )
203    }
204
205    /// Returns the inspect representation of the metadata.
206    fn inspect(&self) -> String {
207        format!(
208            "#<Lindera::Metadata: name='{}', encoding='{}', schema_fields={}>",
209            self.inner.name,
210            self.inner.encoding,
211            self.inner.dictionary_schema.field_count()
212        )
213    }
214}
215
216impl From<RbMetadata> for Metadata {
217    fn from(metadata: RbMetadata) -> Self {
218        metadata.inner.into()
219    }
220}
221
222impl From<Metadata> for RbMetadata {
223    fn from(metadata: Metadata) -> Self {
224        RbMetadata {
225            inner: CoreMetadata::from(metadata),
226        }
227    }
228}
229
230/// Defines Metadata class in the given Ruby module.
231///
232/// # Arguments
233///
234/// * `ruby` - Ruby runtime handle.
235/// * `module` - Parent Ruby module.
236///
237/// # Returns
238///
239/// `Ok(())` on success, or a Magnus `Error` on failure.
240pub fn define(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
241    let metadata_class = module.define_class("Metadata", ruby.class_object())?;
242    metadata_class.define_singleton_method("new", function!(RbMetadata::new, 9))?;
243    metadata_class
244        .define_singleton_method("create_default", function!(RbMetadata::create_default, 0))?;
245    metadata_class
246        .define_singleton_method("from_json_file", function!(RbMetadata::from_json_file, 1))?;
247    metadata_class.define_method("name", method!(RbMetadata::name, 0))?;
248    metadata_class.define_method("encoding", method!(RbMetadata::encoding, 0))?;
249    metadata_class.define_method(
250        "default_word_cost",
251        method!(RbMetadata::default_word_cost, 0),
252    )?;
253    metadata_class.define_method(
254        "default_left_context_id",
255        method!(RbMetadata::default_left_context_id, 0),
256    )?;
257    metadata_class.define_method(
258        "default_right_context_id",
259        method!(RbMetadata::default_right_context_id, 0),
260    )?;
261    metadata_class.define_method(
262        "default_field_value",
263        method!(RbMetadata::default_field_value, 0),
264    )?;
265    metadata_class.define_method("flexible_csv", method!(RbMetadata::flexible_csv, 0))?;
266    metadata_class.define_method(
267        "skip_invalid_cost_or_id",
268        method!(RbMetadata::skip_invalid_cost_or_id, 0),
269    )?;
270    metadata_class.define_method(
271        "normalize_details",
272        method!(RbMetadata::normalize_details, 0),
273    )?;
274    metadata_class.define_method("to_hash", method!(RbMetadata::to_hash, 0))?;
275    metadata_class.define_method("to_h", method!(RbMetadata::to_hash, 0))?;
276    metadata_class.define_method("to_s", method!(RbMetadata::to_s, 0))?;
277    metadata_class.define_method("inspect", method!(RbMetadata::inspect, 0))?;
278
279    Ok(())
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use lindera_binding_core::CoreSchema;
286
287    #[test]
288    fn test_rb_metadata_to_lindera_metadata() {
289        let rb_metadata = RbMetadata {
290            inner: CoreMetadata::new(
291                Some("test_dict".to_string()),
292                Some("EUC-JP".to_string()),
293                Some(-5000),
294                Some(100),
295                Some(200),
296                Some("N/A".to_string()),
297                Some(true),
298                Some(true),
299                Some(true),
300                Some(CoreSchema::new(vec![
301                    "surface".to_string(),
302                    "cost".to_string(),
303                ])),
304                Some(CoreSchema::new(vec!["surface".to_string()])),
305            ),
306        };
307
308        let lindera_metadata: Metadata = rb_metadata.into();
309        assert_eq!(lindera_metadata.name, "test_dict");
310        assert_eq!(lindera_metadata.encoding, "EUC-JP");
311        assert_eq!(lindera_metadata.default_word_cost, -5000);
312        assert_eq!(lindera_metadata.default_left_context_id, 100);
313        assert_eq!(lindera_metadata.default_right_context_id, 200);
314        assert_eq!(lindera_metadata.default_field_value, "N/A");
315        assert!(lindera_metadata.flexible_csv);
316        assert!(lindera_metadata.skip_invalid_cost_or_id);
317        assert!(lindera_metadata.normalize_details);
318        assert_eq!(lindera_metadata.dictionary_schema.get_all_fields().len(), 2);
319        assert_eq!(
320            lindera_metadata
321                .user_dictionary_schema
322                .get_all_fields()
323                .len(),
324            1
325        );
326    }
327
328    #[test]
329    fn test_lindera_metadata_to_rb_metadata() {
330        let dict_schema =
331            lindera::dictionary::Schema::new(vec!["surface".to_string(), "cost".to_string()]);
332        let user_schema =
333            lindera::dictionary::Schema::new(vec!["surface".to_string(), "reading".to_string()]);
334
335        let lindera_metadata = Metadata::new(
336            "my_dict".to_string(),
337            "UTF-8".to_string(),
338            -8000,
339            500,
340            600,
341            "?".to_string(),
342            false,
343            true,
344            false,
345            dict_schema,
346            user_schema,
347        );
348
349        let rb_metadata: RbMetadata = lindera_metadata.into();
350        assert_eq!(rb_metadata.name(), "my_dict");
351        assert_eq!(rb_metadata.encoding(), "UTF-8");
352        assert_eq!(rb_metadata.default_word_cost(), -8000);
353        assert_eq!(rb_metadata.default_left_context_id(), 500);
354        assert_eq!(rb_metadata.default_right_context_id(), 600);
355        assert_eq!(rb_metadata.default_field_value(), "?");
356        assert!(!rb_metadata.flexible_csv());
357        assert!(rb_metadata.skip_invalid_cost_or_id());
358        assert!(!rb_metadata.normalize_details());
359        assert_eq!(rb_metadata.inner.dictionary_schema.field_count(), 2);
360        assert_eq!(rb_metadata.inner.user_dictionary_schema.field_count(), 2);
361    }
362
363    #[test]
364    fn test_rb_metadata_defaults() {
365        let rb_metadata = RbMetadata::create_default();
366        assert_eq!(rb_metadata.name(), "default");
367        assert_eq!(rb_metadata.encoding(), "UTF-8");
368        assert_eq!(rb_metadata.default_word_cost(), -10000);
369        assert_eq!(rb_metadata.default_left_context_id(), 1288);
370        assert_eq!(rb_metadata.default_right_context_id(), 1288);
371        assert_eq!(rb_metadata.default_field_value(), "*");
372        assert!(!rb_metadata.flexible_csv());
373        assert_eq!(rb_metadata.inner.dictionary_schema.field_count(), 13);
374        assert_eq!(rb_metadata.inner.user_dictionary_schema.field_count(), 3);
375    }
376
377    #[test]
378    fn test_rb_metadata_roundtrip() {
379        let rb_metadata = RbMetadata::create_default();
380        let lindera: Metadata = rb_metadata.into();
381        let back: RbMetadata = lindera.into();
382        assert_eq!(back.name(), "default");
383        assert_eq!(back.encoding(), "UTF-8");
384        assert_eq!(back.default_word_cost(), -10000);
385        assert_eq!(back.default_left_context_id(), 1288);
386        assert_eq!(back.default_right_context_id(), 1288);
387        assert_eq!(back.default_field_value(), "*");
388        assert_eq!(back.inner.dictionary_schema.field_count(), 13);
389        assert_eq!(back.inner.user_dictionary_schema.field_count(), 3);
390    }
391}