1use pyo3::prelude::*;
28
29use lindera::dictionary::{FieldDefinition, FieldType, Schema};
30use lindera_binding_core::{CoreFieldDefinition, CoreFieldType, CoreSchema};
31
32use crate::error::to_py_error;
33
34#[pyclass(name = "FieldType", from_py_object)]
38#[derive(Debug, Clone)]
39pub enum PyFieldType {
40 Surface,
42 LeftContextId,
44 RightContextId,
46 Cost,
48 Custom,
50}
51
52#[pymethods]
53impl PyFieldType {
54 fn __str__(&self) -> &str {
55 match self {
56 PyFieldType::Surface => "surface",
57 PyFieldType::LeftContextId => "left_context_id",
58 PyFieldType::RightContextId => "right_context_id",
59 PyFieldType::Cost => "cost",
60 PyFieldType::Custom => "custom",
61 }
62 }
63
64 fn __repr__(&self) -> String {
65 format!("FieldType.{self:?}")
66 }
67}
68
69impl From<CoreFieldType> for PyFieldType {
70 fn from(field_type: CoreFieldType) -> Self {
71 match field_type {
72 CoreFieldType::Surface => PyFieldType::Surface,
73 CoreFieldType::LeftContextId => PyFieldType::LeftContextId,
74 CoreFieldType::RightContextId => PyFieldType::RightContextId,
75 CoreFieldType::Cost => PyFieldType::Cost,
76 CoreFieldType::Custom => PyFieldType::Custom,
77 }
78 }
79}
80
81impl From<PyFieldType> for CoreFieldType {
82 fn from(field_type: PyFieldType) -> Self {
83 match field_type {
84 PyFieldType::Surface => CoreFieldType::Surface,
85 PyFieldType::LeftContextId => CoreFieldType::LeftContextId,
86 PyFieldType::RightContextId => CoreFieldType::RightContextId,
87 PyFieldType::Cost => CoreFieldType::Cost,
88 PyFieldType::Custom => CoreFieldType::Custom,
89 }
90 }
91}
92
93impl From<FieldType> for PyFieldType {
94 fn from(field_type: FieldType) -> Self {
95 PyFieldType::from(CoreFieldType::from(field_type))
96 }
97}
98
99impl From<PyFieldType> for FieldType {
100 fn from(field_type: PyFieldType) -> Self {
101 FieldType::from(CoreFieldType::from(field_type))
102 }
103}
104
105#[pyclass(name = "FieldDefinition", from_py_object)]
109#[derive(Debug, Clone)]
110pub struct PyFieldDefinition {
111 #[pyo3(get)]
112 pub index: usize,
113 #[pyo3(get)]
114 pub name: String,
115 #[pyo3(get)]
116 pub field_type: PyFieldType,
117 #[pyo3(get)]
118 pub description: Option<String>,
119}
120
121#[pymethods]
122impl PyFieldDefinition {
123 #[new]
124 pub fn new(
125 index: usize,
126 name: String,
127 field_type: PyFieldType,
128 description: Option<String>,
129 ) -> Self {
130 Self {
131 index,
132 name,
133 field_type,
134 description,
135 }
136 }
137
138 fn __str__(&self) -> String {
139 format!("FieldDefinition(index={}, name={})", self.index, self.name)
140 }
141
142 fn __repr__(&self) -> String {
143 format!(
144 "FieldDefinition(index={}, name='{}', field_type={:?}, description={:?})",
145 self.index, self.name, self.field_type, self.description
146 )
147 }
148}
149
150impl From<CoreFieldDefinition> for PyFieldDefinition {
151 fn from(field_def: CoreFieldDefinition) -> Self {
152 PyFieldDefinition {
153 index: field_def.index,
154 name: field_def.name,
155 field_type: field_def.field_type.into(),
156 description: field_def.description,
157 }
158 }
159}
160
161impl From<PyFieldDefinition> for CoreFieldDefinition {
162 fn from(field_def: PyFieldDefinition) -> Self {
163 CoreFieldDefinition {
164 index: field_def.index,
165 name: field_def.name,
166 field_type: field_def.field_type.into(),
167 description: field_def.description,
168 }
169 }
170}
171
172impl From<FieldDefinition> for PyFieldDefinition {
173 fn from(field_def: FieldDefinition) -> Self {
174 PyFieldDefinition::from(CoreFieldDefinition::from(field_def))
175 }
176}
177
178impl From<PyFieldDefinition> for FieldDefinition {
179 fn from(field_def: PyFieldDefinition) -> Self {
180 FieldDefinition::from(CoreFieldDefinition::from(field_def))
181 }
182}
183
184#[pyclass(name = "Schema", from_py_object)]
200#[derive(Debug, Clone)]
201pub struct PySchema {
202 pub inner: CoreSchema,
204}
205
206#[pymethods]
207impl PySchema {
208 #[new]
209 pub fn new(fields: Vec<String>) -> Self {
210 Self {
211 inner: CoreSchema::new(fields),
212 }
213 }
214
215 #[staticmethod]
216 pub fn create_default() -> Self {
217 Self {
218 inner: CoreSchema::create_default(),
219 }
220 }
221
222 #[getter]
223 pub fn fields(&self) -> Vec<String> {
224 self.inner.fields().to_vec()
225 }
226
227 pub fn get_field_index(&self, field_name: &str) -> Option<usize> {
228 self.inner.get_field_index(field_name)
229 }
230
231 pub fn field_count(&self) -> usize {
232 self.inner.field_count()
233 }
234
235 pub fn get_field_name(&self, index: usize) -> Option<&str> {
236 self.inner.get_field_name(index)
237 }
238
239 pub fn get_custom_fields(&self) -> Vec<String> {
240 self.inner.get_custom_fields().to_vec()
241 }
242
243 pub fn get_all_fields(&self) -> Vec<String> {
244 self.inner.fields().to_vec()
245 }
246
247 pub fn get_field_by_name(&self, name: &str) -> Option<PyFieldDefinition> {
248 self.inner
249 .get_field_by_name(name)
250 .map(PyFieldDefinition::from)
251 }
252
253 pub fn validate_record(&self, record: Vec<String>) -> PyResult<()> {
254 self.inner.validate_record(&record).map_err(to_py_error)
255 }
256
257 fn __str__(&self) -> String {
258 format!("Schema(fields={})", self.inner.field_count())
259 }
260
261 fn __repr__(&self) -> String {
262 format!("Schema(fields={:?})", self.inner.fields())
263 }
264
265 fn __len__(&self) -> usize {
266 self.inner.field_count()
267 }
268}
269
270impl From<CoreSchema> for PySchema {
271 fn from(schema: CoreSchema) -> Self {
272 PySchema { inner: schema }
273 }
274}
275
276impl From<PySchema> for CoreSchema {
277 fn from(schema: PySchema) -> Self {
278 schema.inner
279 }
280}
281
282impl From<PySchema> for Schema {
283 fn from(schema: PySchema) -> Self {
284 schema.inner.into()
285 }
286}
287
288impl From<Schema> for PySchema {
289 fn from(schema: Schema) -> Self {
290 PySchema {
291 inner: CoreSchema::from(schema),
292 }
293 }
294}
295
296pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
297 let py = parent_module.py();
298 let m = PyModule::new(py, "schema")?;
299 m.add_class::<PySchema>()?;
300 m.add_class::<PyFieldDefinition>()?;
301 m.add_class::<PyFieldType>()?;
302 parent_module.add_submodule(&m)?;
303 Ok(())
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use lindera::dictionary::{FieldDefinition, FieldType, Schema};
310
311 #[test]
312 fn test_pyfieldtype_to_fieldtype_all_variants() {
313 for (py, ft) in [
314 (PyFieldType::Surface, FieldType::Surface),
315 (PyFieldType::LeftContextId, FieldType::LeftContextId),
316 (PyFieldType::RightContextId, FieldType::RightContextId),
317 (PyFieldType::Cost, FieldType::Cost),
318 (PyFieldType::Custom, FieldType::Custom),
319 ] {
320 let converted: FieldType = py.into();
321 assert_eq!(converted, ft);
322 }
323 }
324
325 #[test]
326 fn test_fieldtype_to_pyfieldtype_all_variants() {
327 assert!(matches!(
328 PyFieldType::from(FieldType::Surface),
329 PyFieldType::Surface
330 ));
331 assert!(matches!(
332 PyFieldType::from(FieldType::Custom),
333 PyFieldType::Custom
334 ));
335 }
336
337 #[test]
338 fn test_pyfielddefinition_to_fielddefinition() {
339 let py_fd = PyFieldDefinition {
340 index: 0,
341 name: "surface".to_string(),
342 field_type: PyFieldType::Surface,
343 description: Some("Surface form".to_string()),
344 };
345 let fd: FieldDefinition = py_fd.into();
346 assert_eq!(fd.index, 0);
347 assert_eq!(fd.name, "surface");
348 assert!(matches!(fd.field_type, FieldType::Surface));
349 assert_eq!(fd.description, Some("Surface form".to_string()));
350 }
351
352 #[test]
353 fn test_fielddefinition_to_pyfielddefinition() {
354 let fd = FieldDefinition {
355 index: 4,
356 name: "pos".to_string(),
357 field_type: FieldType::Custom,
358 description: None,
359 };
360 let py_fd: PyFieldDefinition = fd.into();
361 assert_eq!(py_fd.index, 4);
362 assert_eq!(py_fd.name, "pos");
363 assert!(matches!(py_fd.field_type, PyFieldType::Custom));
364 assert!(py_fd.description.is_none());
365 }
366
367 #[test]
368 fn test_pyschema_to_schema() {
369 let py_schema = PySchema::new(vec![
370 "surface".to_string(),
371 "left_context_id".to_string(),
372 "right_context_id".to_string(),
373 "cost".to_string(),
374 "pos".to_string(),
375 ]);
376 let schema: Schema = py_schema.into();
377 let fields = schema.get_all_fields();
378 assert_eq!(fields.len(), 5);
379 assert_eq!(fields[0], "surface");
380 assert_eq!(fields[4], "pos");
381 }
382
383 #[test]
384 fn test_schema_to_pyschema() {
385 let schema = Schema::new(vec![
386 "surface".to_string(),
387 "left_context_id".to_string(),
388 "right_context_id".to_string(),
389 "cost".to_string(),
390 ]);
391 let py_schema: PySchema = schema.into();
392 assert_eq!(py_schema.fields().len(), 4);
393 assert_eq!(py_schema.fields()[0], "surface");
394 }
395
396 #[test]
397 fn test_pyschema_index_and_name_lookups() {
398 let schema = PySchema::new(vec![
399 "surface".to_string(),
400 "pos".to_string(),
401 "reading".to_string(),
402 ]);
403 assert_eq!(schema.get_field_index("surface"), Some(0));
404 assert_eq!(schema.get_field_index("reading"), Some(2));
405 assert_eq!(schema.get_field_index("nonexistent"), None);
406 assert_eq!(schema.get_field_name(1), Some("pos"));
407 assert_eq!(schema.get_field_name(9), None);
408 assert_eq!(schema.field_count(), 3);
409 }
410
411 #[test]
412 fn test_pyschema_custom_fields() {
413 let schema = PySchema::new(vec![
414 "surface".to_string(),
415 "left_context_id".to_string(),
416 "right_context_id".to_string(),
417 "cost".to_string(),
418 "major_pos".to_string(),
419 "reading".to_string(),
420 ]);
421 let custom = schema.get_custom_fields();
422 assert_eq!(custom, ["major_pos", "reading"]);
423 }
424
425 #[test]
426 fn test_pyschema_create_default() {
427 let schema = PySchema::create_default();
428 assert_eq!(schema.field_count(), 13);
429 assert_eq!(schema.fields()[0], "surface");
430 assert_eq!(schema.fields()[5], "pos_detail_1");
431 assert_eq!(schema.fields()[12], "pronunciation");
432 assert_eq!(schema.get_field_index("cost"), Some(3));
433 }
434
435 #[test]
436 fn test_pyschema_get_field_by_name() {
437 let schema = PySchema::create_default();
438 let surface = schema.get_field_by_name("surface").unwrap();
439 assert_eq!(surface.index, 0);
440 assert!(matches!(surface.field_type, PyFieldType::Surface));
441
442 let custom = schema.get_field_by_name("major_pos").unwrap();
443 assert_eq!(custom.index, 4);
444 assert!(matches!(custom.field_type, PyFieldType::Custom));
445
446 assert!(schema.get_field_by_name("nonexistent").is_none());
447 }
448
449 #[test]
458 fn test_pyschema_roundtrip() {
459 let fields = vec![
460 "surface".to_string(),
461 "left_context_id".to_string(),
462 "right_context_id".to_string(),
463 "cost".to_string(),
464 "pos".to_string(),
465 ];
466 let py_schema = PySchema::new(fields.clone());
467 let schema: Schema = py_schema.into();
468 let roundtripped: PySchema = schema.into();
469 assert_eq!(roundtripped.fields(), fields);
470 }
471}