1use std::collections::HashMap;
25
26use pyo3::prelude::*;
27
28use lindera::dictionary::Metadata;
29use lindera_binding_core::CoreMetadata;
30
31use crate::schema::PySchema;
32
33#[pyclass(name = "Metadata", from_py_object)]
52#[derive(Debug, Clone)]
53pub struct PyMetadata {
54 pub inner: CoreMetadata,
56}
57
58#[pymethods]
59impl PyMetadata {
60 #[new]
61 #[pyo3(signature = (name=None, encoding=None, default_word_cost=None, default_left_context_id=None, default_right_context_id=None, default_field_value=None, flexible_csv=None, skip_invalid_cost_or_id=None, normalize_details=None, dictionary_schema=None, user_dictionary_schema=None))]
62 #[allow(clippy::too_many_arguments)]
63 pub fn new(
64 name: Option<String>,
65 encoding: Option<String>,
66 default_word_cost: Option<i16>,
67 default_left_context_id: Option<u16>,
68 default_right_context_id: Option<u16>,
69 default_field_value: Option<String>,
70 flexible_csv: Option<bool>,
71 skip_invalid_cost_or_id: Option<bool>,
72 normalize_details: Option<bool>,
73 dictionary_schema: Option<PySchema>,
74 user_dictionary_schema: Option<PySchema>,
75 ) -> Self {
76 PyMetadata {
77 inner: CoreMetadata::new(
78 name,
79 encoding,
80 default_word_cost,
81 default_left_context_id,
82 default_right_context_id,
83 default_field_value,
84 flexible_csv,
85 skip_invalid_cost_or_id,
86 normalize_details,
87 dictionary_schema.map(Into::into),
88 user_dictionary_schema.map(Into::into),
89 ),
90 }
91 }
92
93 #[staticmethod]
94 pub fn create_default() -> Self {
95 PyMetadata {
96 inner: CoreMetadata::create_default(),
97 }
98 }
99
100 #[staticmethod]
101 pub fn from_json_file(path: &str) -> PyResult<Self> {
102 use std::fs;
103
104 let json_str = fs::read_to_string(path).map_err(|e| {
105 pyo3::exceptions::PyIOError::new_err(format!("Failed to read file: {e}"))
106 })?;
107
108 let metadata: Metadata = serde_json::from_str(&json_str).map_err(|e| {
109 pyo3::exceptions::PyValueError::new_err(format!("Failed to parse JSON: {e}"))
110 })?;
111
112 Ok(metadata.into())
113 }
114
115 #[getter]
116 pub fn name(&self) -> &str {
117 &self.inner.name
118 }
119
120 #[setter]
121 pub fn set_name(&mut self, name: String) {
122 self.inner.name = name;
123 }
124
125 #[getter]
126 pub fn encoding(&self) -> &str {
127 &self.inner.encoding
128 }
129
130 #[setter]
131 pub fn set_encoding(&mut self, encoding: String) {
132 self.inner.encoding = encoding;
133 }
134
135 #[getter]
136 pub fn default_word_cost(&self) -> i16 {
137 self.inner.default_word_cost
138 }
139
140 #[setter]
141 pub fn set_default_word_cost(&mut self, cost: i16) {
142 self.inner.default_word_cost = cost;
143 }
144
145 #[getter]
146 pub fn default_left_context_id(&self) -> u16 {
147 self.inner.default_left_context_id
148 }
149
150 #[setter]
151 pub fn set_default_left_context_id(&mut self, id: u16) {
152 self.inner.default_left_context_id = id;
153 }
154
155 #[getter]
156 pub fn default_right_context_id(&self) -> u16 {
157 self.inner.default_right_context_id
158 }
159
160 #[setter]
161 pub fn set_default_right_context_id(&mut self, id: u16) {
162 self.inner.default_right_context_id = id;
163 }
164
165 #[getter]
166 pub fn default_field_value(&self) -> &str {
167 &self.inner.default_field_value
168 }
169
170 #[setter]
171 pub fn set_default_field_value(&mut self, value: String) {
172 self.inner.default_field_value = value;
173 }
174
175 #[getter]
176 pub fn flexible_csv(&self) -> bool {
177 self.inner.flexible_csv
178 }
179
180 #[setter]
181 pub fn set_flexible_csv(&mut self, value: bool) {
182 self.inner.flexible_csv = value;
183 }
184
185 #[getter]
186 pub fn skip_invalid_cost_or_id(&self) -> bool {
187 self.inner.skip_invalid_cost_or_id
188 }
189
190 #[setter]
191 pub fn set_skip_invalid_cost_or_id(&mut self, value: bool) {
192 self.inner.skip_invalid_cost_or_id = value;
193 }
194
195 #[getter]
196 pub fn normalize_details(&self) -> bool {
197 self.inner.normalize_details
198 }
199
200 #[setter]
201 pub fn set_normalize_details(&mut self, value: bool) {
202 self.inner.normalize_details = value;
203 }
204
205 #[getter]
206 pub fn dictionary_schema(&self) -> PySchema {
207 PySchema::from(self.inner.dictionary_schema.clone())
208 }
209
210 #[setter]
211 pub fn set_dictionary_schema(&mut self, schema: PySchema) {
212 self.inner.dictionary_schema = schema.into();
213 }
214
215 #[getter]
216 pub fn user_dictionary_schema(&self) -> PySchema {
217 PySchema::from(self.inner.user_dictionary_schema.clone())
218 }
219
220 #[setter]
221 pub fn set_user_dictionary_schema(&mut self, schema: PySchema) {
222 self.inner.user_dictionary_schema = schema.into();
223 }
224
225 pub fn to_dict(&self) -> HashMap<String, String> {
226 let mut dict = HashMap::new();
227 dict.insert("name".to_string(), self.inner.name.clone());
228 dict.insert("encoding".to_string(), self.inner.encoding.clone());
229 dict.insert(
230 "default_word_cost".to_string(),
231 self.inner.default_word_cost.to_string(),
232 );
233 dict.insert(
234 "default_left_context_id".to_string(),
235 self.inner.default_left_context_id.to_string(),
236 );
237 dict.insert(
238 "default_right_context_id".to_string(),
239 self.inner.default_right_context_id.to_string(),
240 );
241 dict.insert(
242 "default_field_value".to_string(),
243 self.inner.default_field_value.clone(),
244 );
245 dict.insert(
246 "flexible_csv".to_string(),
247 self.inner.flexible_csv.to_string(),
248 );
249 dict.insert(
250 "skip_invalid_cost_or_id".to_string(),
251 self.inner.skip_invalid_cost_or_id.to_string(),
252 );
253 dict.insert(
254 "normalize_details".to_string(),
255 self.inner.normalize_details.to_string(),
256 );
257 dict.insert(
258 "dictionary_schema_fields".to_string(),
259 self.inner.dictionary_schema.fields().join(","),
260 );
261 dict.insert(
262 "user_dictionary_schema_fields".to_string(),
263 self.inner.user_dictionary_schema.fields().join(","),
264 );
265 dict
266 }
267
268 fn __str__(&self) -> String {
269 format!(
270 "Metadata(name='{}', encoding='{}')",
271 self.inner.name, self.inner.encoding,
272 )
273 }
274
275 fn __repr__(&self) -> String {
276 format!(
277 "Metadata(name='{}', encoding='{}', schema_fields={})",
278 self.inner.name,
279 self.inner.encoding,
280 self.inner.dictionary_schema.field_count()
281 )
282 }
283}
284
285impl From<PyMetadata> for Metadata {
286 fn from(metadata: PyMetadata) -> Self {
287 metadata.inner.into()
288 }
289}
290
291impl From<Metadata> for PyMetadata {
292 fn from(metadata: Metadata) -> Self {
293 PyMetadata {
294 inner: CoreMetadata::from(metadata),
295 }
296 }
297}
298
299pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
300 let py = parent_module.py();
301 let m = PyModule::new(py, "metadata")?;
302 m.add_class::<PyMetadata>()?;
303 parent_module.add_submodule(&m)?;
304 Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use lindera::dictionary::Metadata;
311
312 #[test]
313 fn test_pymetadata_to_metadata() {
314 let py_meta = PyMetadata::new(
315 Some("test_dict".to_string()),
316 Some("EUC-JP".to_string()),
317 Some(-5000),
318 Some(100),
319 Some(200),
320 Some("N/A".to_string()),
321 Some(true),
322 Some(true),
323 Some(true),
324 None,
325 None,
326 );
327 let meta: Metadata = py_meta.into();
328 assert_eq!(meta.name, "test_dict");
329 assert_eq!(meta.encoding, "EUC-JP");
330 assert_eq!(meta.default_word_cost, -5000);
331 assert_eq!(meta.default_left_context_id, 100);
332 assert_eq!(meta.default_right_context_id, 200);
333 assert_eq!(meta.default_field_value, "N/A");
334 assert!(meta.flexible_csv);
335 assert!(meta.skip_invalid_cost_or_id);
336 assert!(meta.normalize_details);
337 }
338
339 #[test]
340 fn test_metadata_to_pymetadata() {
341 let schema = lindera::dictionary::Schema::new(vec![
342 "surface".to_string(),
343 "left_context_id".to_string(),
344 "right_context_id".to_string(),
345 "cost".to_string(),
346 ]);
347 let userdic_schema =
348 lindera::dictionary::Schema::new(vec!["surface".to_string(), "reading".to_string()]);
349 let meta = Metadata::new(
350 "my_dict".to_string(),
351 "UTF-8".to_string(),
352 -10000,
353 1288,
354 1288,
355 "*".to_string(),
356 false,
357 false,
358 false,
359 schema,
360 userdic_schema,
361 );
362 let py_meta: PyMetadata = meta.into();
363 assert_eq!(py_meta.name(), "my_dict");
364 assert_eq!(py_meta.encoding(), "UTF-8");
365 assert_eq!(py_meta.default_word_cost(), -10000);
366 assert_eq!(py_meta.default_left_context_id(), 1288);
367 assert_eq!(py_meta.default_right_context_id(), 1288);
368 assert_eq!(py_meta.default_field_value(), "*");
369 assert!(!py_meta.flexible_csv());
370 assert!(!py_meta.skip_invalid_cost_or_id());
371 assert!(!py_meta.normalize_details());
372 assert_eq!(py_meta.dictionary_schema().fields().len(), 4);
373 assert_eq!(py_meta.user_dictionary_schema().fields().len(), 2);
374 }
375
376 #[test]
377 fn test_pymetadata_default_values() {
378 let py_meta = PyMetadata::create_default();
379 assert_eq!(py_meta.name(), "default");
380 assert_eq!(py_meta.encoding(), "UTF-8");
381 assert_eq!(py_meta.default_word_cost(), -10000);
382 assert_eq!(py_meta.default_left_context_id(), 1288);
383 assert_eq!(py_meta.default_right_context_id(), 1288);
384 assert_eq!(py_meta.default_field_value(), "*");
385 assert!(!py_meta.flexible_csv());
386 assert!(!py_meta.skip_invalid_cost_or_id());
387 assert!(!py_meta.normalize_details());
388 assert_eq!(py_meta.dictionary_schema().field_count(), 13);
389 assert_eq!(py_meta.user_dictionary_schema().fields().len(), 3);
390 }
391
392 #[test]
393 fn test_pymetadata_roundtrip() {
394 let py_meta = PyMetadata::new(
395 Some("roundtrip".to_string()),
396 Some("UTF-8".to_string()),
397 Some(-8000),
398 Some(500),
399 Some(600),
400 Some("?".to_string()),
401 Some(true),
402 Some(false),
403 Some(true),
404 None,
405 None,
406 );
407 let meta: Metadata = py_meta.into();
408 let roundtripped: PyMetadata = meta.into();
409 assert_eq!(roundtripped.name(), "roundtrip");
410 assert_eq!(roundtripped.encoding(), "UTF-8");
411 assert_eq!(roundtripped.default_word_cost(), -8000);
412 assert_eq!(roundtripped.default_left_context_id(), 500);
413 assert_eq!(roundtripped.default_right_context_id(), 600);
414 assert_eq!(roundtripped.default_field_value(), "?");
415 assert!(roundtripped.flexible_csv());
416 assert!(!roundtripped.skip_invalid_cost_or_id());
417 assert!(roundtripped.normalize_details());
418 }
419}