Skip to main content

lindera_binding_core/
metadata.rs

1//! Shared dictionary-metadata defaults and schema wiring for the bindings.
2//!
3//! The Python/PHP/Ruby/Node.js `Metadata` wrappers each hard-coded the same
4//! default values (name `"default"`, encoding `"UTF-8"`, word cost `-10000`,
5//! context ids `1288`, field value `"*"`, three `false` flags) and the same
6//! default user-dictionary schema (`surface`/`reading`/`pronunciation`). This
7//! module collects that into a single [`CoreMetadata`] the bindings can wrap.
8
9use lindera::dictionary::Metadata;
10
11use crate::schema::CoreSchema;
12
13/// Default dictionary name.
14const DEFAULT_NAME: &str = "default";
15/// Default character encoding.
16const DEFAULT_ENCODING: &str = "UTF-8";
17/// Default cost assigned to simple user-dictionary entries.
18const DEFAULT_WORD_COST: i16 = -10000;
19/// Default left context id for simple user-dictionary entries.
20const DEFAULT_LEFT_CONTEXT_ID: u16 = 1288;
21/// Default right context id for simple user-dictionary entries.
22const DEFAULT_RIGHT_CONTEXT_ID: u16 = 1288;
23/// Default value substituted for missing fields.
24const DEFAULT_FIELD_VALUE: &str = "*";
25
26/// Returns the default user-dictionary schema (`surface`/`reading`/`pronunciation`).
27fn default_user_dictionary_schema() -> CoreSchema {
28    CoreSchema::new(vec![
29        "surface".to_string(),
30        "reading".to_string(),
31        "pronunciation".to_string(),
32    ])
33}
34
35/// Dictionary metadata shared by the bindings.
36///
37/// Mirrors the binding `Metadata` wrappers as a pure-Rust type (using
38/// [`CoreSchema`] for the two schema fields) so each binding can wrap a
39/// `CoreMetadata` instead of re-declaring the same defaults and conversions.
40/// Converts to and from [`lindera::dictionary::Metadata`]; the optional
41/// `model_info` carried by the lindera type is not retained (the bindings do
42/// not expose it).
43#[derive(Debug, Clone)]
44pub struct CoreMetadata {
45    /// Dictionary name.
46    pub name: String,
47    /// Character encoding.
48    pub encoding: String,
49    /// Default word cost for simple user-dictionary entries.
50    pub default_word_cost: i16,
51    /// Default left context id for simple user-dictionary entries.
52    pub default_left_context_id: u16,
53    /// Default right context id for simple user-dictionary entries.
54    pub default_right_context_id: u16,
55    /// Default value substituted for missing fields.
56    pub default_field_value: String,
57    /// Whether CSV columns are handled flexibly.
58    pub flexible_csv: bool,
59    /// Whether entries with invalid cost or id are skipped.
60    pub skip_invalid_cost_or_id: bool,
61    /// Whether morphological details are normalized.
62    pub normalize_details: bool,
63    /// Schema for the main dictionary.
64    pub dictionary_schema: CoreSchema,
65    /// Schema for the user dictionary.
66    pub user_dictionary_schema: CoreSchema,
67}
68
69impl CoreMetadata {
70    /// Creates metadata, falling back to the binding defaults for any `None`.
71    #[allow(clippy::too_many_arguments)]
72    pub fn new(
73        name: Option<String>,
74        encoding: Option<String>,
75        default_word_cost: Option<i16>,
76        default_left_context_id: Option<u16>,
77        default_right_context_id: Option<u16>,
78        default_field_value: Option<String>,
79        flexible_csv: Option<bool>,
80        skip_invalid_cost_or_id: Option<bool>,
81        normalize_details: Option<bool>,
82        dictionary_schema: Option<CoreSchema>,
83        user_dictionary_schema: Option<CoreSchema>,
84    ) -> Self {
85        Self {
86            name: name.unwrap_or_else(|| DEFAULT_NAME.to_string()),
87            encoding: encoding.unwrap_or_else(|| DEFAULT_ENCODING.to_string()),
88            default_word_cost: default_word_cost.unwrap_or(DEFAULT_WORD_COST),
89            default_left_context_id: default_left_context_id.unwrap_or(DEFAULT_LEFT_CONTEXT_ID),
90            default_right_context_id: default_right_context_id.unwrap_or(DEFAULT_RIGHT_CONTEXT_ID),
91            default_field_value: default_field_value
92                .unwrap_or_else(|| DEFAULT_FIELD_VALUE.to_string()),
93            flexible_csv: flexible_csv.unwrap_or(false),
94            skip_invalid_cost_or_id: skip_invalid_cost_or_id.unwrap_or(false),
95            normalize_details: normalize_details.unwrap_or(false),
96            dictionary_schema: dictionary_schema.unwrap_or_else(CoreSchema::create_default),
97            user_dictionary_schema: user_dictionary_schema
98                .unwrap_or_else(default_user_dictionary_schema),
99        }
100    }
101
102    /// Creates metadata with all binding defaults.
103    pub fn create_default() -> Self {
104        Self::new(
105            None, None, None, None, None, None, None, None, None, None, None,
106        )
107    }
108}
109
110impl Default for CoreMetadata {
111    /// Returns [`CoreMetadata::create_default`].
112    fn default() -> Self {
113        Self::create_default()
114    }
115}
116
117impl From<Metadata> for CoreMetadata {
118    /// Converts a lindera [`Metadata`] into a [`CoreMetadata`] (dropping `model_info`).
119    fn from(metadata: Metadata) -> Self {
120        Self {
121            name: metadata.name,
122            encoding: metadata.encoding,
123            default_word_cost: metadata.default_word_cost,
124            default_left_context_id: metadata.default_left_context_id,
125            default_right_context_id: metadata.default_right_context_id,
126            default_field_value: metadata.default_field_value,
127            flexible_csv: metadata.flexible_csv,
128            skip_invalid_cost_or_id: metadata.skip_invalid_cost_or_id,
129            normalize_details: metadata.normalize_details,
130            dictionary_schema: metadata.dictionary_schema.into(),
131            user_dictionary_schema: metadata.user_dictionary_schema.into(),
132        }
133    }
134}
135
136impl From<CoreMetadata> for Metadata {
137    /// Converts a [`CoreMetadata`] into a lindera [`Metadata`] (`model_info` is `None`).
138    fn from(metadata: CoreMetadata) -> Self {
139        Metadata::new(
140            metadata.name,
141            metadata.encoding,
142            metadata.default_word_cost,
143            metadata.default_left_context_id,
144            metadata.default_right_context_id,
145            metadata.default_field_value,
146            metadata.flexible_csv,
147            metadata.skip_invalid_cost_or_id,
148            metadata.normalize_details,
149            metadata.dictionary_schema.into(),
150            metadata.user_dictionary_schema.into(),
151        )
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn create_default_uses_binding_defaults() {
161        let m = CoreMetadata::create_default();
162        assert_eq!(m.name, "default");
163        assert_eq!(m.encoding, "UTF-8");
164        assert_eq!(m.default_word_cost, -10000);
165        assert_eq!(m.default_left_context_id, 1288);
166        assert_eq!(m.default_right_context_id, 1288);
167        assert_eq!(m.default_field_value, "*");
168        assert!(!m.flexible_csv);
169        assert!(!m.skip_invalid_cost_or_id);
170        assert!(!m.normalize_details);
171        // The default dictionary schema uses the unified `pos_detail_*` names.
172        assert_eq!(m.dictionary_schema.field_count(), 13);
173        assert_eq!(m.dictionary_schema.fields()[5], "pos_detail_1");
174        assert_eq!(m.user_dictionary_schema.fields().len(), 3);
175        assert_eq!(m.user_dictionary_schema.fields()[1], "reading");
176    }
177
178    #[test]
179    fn new_applies_overrides() {
180        let m = CoreMetadata::new(
181            Some("custom".to_string()),
182            Some("EUC-JP".to_string()),
183            Some(-5000),
184            Some(100),
185            Some(200),
186            Some("N/A".to_string()),
187            Some(true),
188            Some(true),
189            Some(true),
190            None,
191            None,
192        );
193        assert_eq!(m.name, "custom");
194        assert_eq!(m.encoding, "EUC-JP");
195        assert_eq!(m.default_word_cost, -5000);
196        assert_eq!(m.default_left_context_id, 100);
197        assert_eq!(m.default_right_context_id, 200);
198        assert_eq!(m.default_field_value, "N/A");
199        assert!(m.flexible_csv && m.skip_invalid_cost_or_id && m.normalize_details);
200    }
201
202    #[test]
203    fn converts_to_and_from_lindera() {
204        let m = CoreMetadata::create_default();
205        let lindera: Metadata = m.into();
206        assert_eq!(lindera.name, "default");
207        assert_eq!(lindera.default_left_context_id, 1288);
208        assert_eq!(lindera.user_dictionary_schema.field_count(), 3);
209
210        let back: CoreMetadata = lindera.into();
211        assert_eq!(back.name, "default");
212        assert_eq!(back.dictionary_schema.fields()[5], "pos_detail_1");
213    }
214}