Skip to main content

lindera_ruby/
schema.rs

1//! Dictionary schema definitions.
2//!
3//! This module provides schema structures that define the format and fields
4//! of dictionary entries. The field-management logic is delegated to
5//! [`lindera_binding_core::CoreSchema`]; this module only adds the magnus wrappers.
6
7use magnus::prelude::*;
8use magnus::{Error, RArray, Ruby, function, method};
9
10use lindera::dictionary::{FieldDefinition, FieldType, Schema};
11use lindera_binding_core::{CoreFieldDefinition, CoreFieldType, CoreSchema};
12
13/// Field type in dictionary schema.
14///
15/// Defines the type of a field in the dictionary entry.
16#[magnus::wrap(class = "Lindera::FieldType", free_immediately, size)]
17#[derive(Debug, Clone)]
18pub struct RbFieldType {
19    /// Internal field type variant.
20    inner: RbFieldTypeKind,
21}
22
23/// Internal enum for field type kind.
24#[derive(Debug, Clone)]
25enum RbFieldTypeKind {
26    /// Surface form (word text).
27    Surface,
28    /// Left context ID for morphological analysis.
29    LeftContextId,
30    /// Right context ID for morphological analysis.
31    RightContextId,
32    /// Word cost (used in path selection).
33    Cost,
34    /// Custom field (morphological features).
35    Custom,
36}
37
38impl RbFieldType {
39    /// Returns the string representation of the field type.
40    fn to_s(&self) -> &str {
41        match self.inner {
42            RbFieldTypeKind::Surface => "surface",
43            RbFieldTypeKind::LeftContextId => "left_context_id",
44            RbFieldTypeKind::RightContextId => "right_context_id",
45            RbFieldTypeKind::Cost => "cost",
46            RbFieldTypeKind::Custom => "custom",
47        }
48    }
49
50    /// Returns the inspect representation of the field type.
51    fn inspect(&self) -> String {
52        format!("#<Lindera::FieldType: {}>", self.to_s())
53    }
54}
55
56impl From<CoreFieldType> for RbFieldType {
57    fn from(field_type: CoreFieldType) -> Self {
58        let inner = match field_type {
59            CoreFieldType::Surface => RbFieldTypeKind::Surface,
60            CoreFieldType::LeftContextId => RbFieldTypeKind::LeftContextId,
61            CoreFieldType::RightContextId => RbFieldTypeKind::RightContextId,
62            CoreFieldType::Cost => RbFieldTypeKind::Cost,
63            CoreFieldType::Custom => RbFieldTypeKind::Custom,
64        };
65        RbFieldType { inner }
66    }
67}
68
69impl From<RbFieldType> for CoreFieldType {
70    fn from(field_type: RbFieldType) -> Self {
71        match field_type.inner {
72            RbFieldTypeKind::Surface => CoreFieldType::Surface,
73            RbFieldTypeKind::LeftContextId => CoreFieldType::LeftContextId,
74            RbFieldTypeKind::RightContextId => CoreFieldType::RightContextId,
75            RbFieldTypeKind::Cost => CoreFieldType::Cost,
76            RbFieldTypeKind::Custom => CoreFieldType::Custom,
77        }
78    }
79}
80
81impl From<FieldType> for RbFieldType {
82    fn from(field_type: FieldType) -> Self {
83        RbFieldType::from(CoreFieldType::from(field_type))
84    }
85}
86
87impl From<RbFieldType> for FieldType {
88    fn from(field_type: RbFieldType) -> Self {
89        FieldType::from(CoreFieldType::from(field_type))
90    }
91}
92
93/// Field definition in dictionary schema.
94///
95/// Describes a single field in the dictionary entry format.
96#[magnus::wrap(class = "Lindera::FieldDefinition", free_immediately, size)]
97#[derive(Debug, Clone)]
98pub struct RbFieldDefinition {
99    /// Field index in the schema.
100    pub index: usize,
101    /// Field name.
102    pub name: String,
103    /// Field type.
104    pub field_type: RbFieldType,
105    /// Optional description.
106    pub description: Option<String>,
107}
108
109impl RbFieldDefinition {
110    /// Returns the index of the field.
111    fn index(&self) -> usize {
112        self.index
113    }
114
115    /// Returns the name of the field.
116    fn name(&self) -> String {
117        self.name.clone()
118    }
119
120    /// Returns the field type.
121    fn field_type(&self) -> RbFieldType {
122        self.field_type.clone()
123    }
124
125    /// Returns the description of the field.
126    fn description(&self) -> Option<String> {
127        self.description.clone()
128    }
129
130    /// Returns the string representation of the field definition.
131    fn to_s(&self) -> String {
132        format!("FieldDefinition(index={}, name={})", self.index, self.name)
133    }
134
135    /// Returns the inspect representation of the field definition.
136    fn inspect(&self) -> String {
137        format!(
138            "#<Lindera::FieldDefinition: index={}, name='{}', field_type={:?}, description={:?}>",
139            self.index, self.name, self.field_type.inner, self.description
140        )
141    }
142}
143
144impl From<CoreFieldDefinition> for RbFieldDefinition {
145    fn from(field_def: CoreFieldDefinition) -> Self {
146        RbFieldDefinition {
147            index: field_def.index,
148            name: field_def.name,
149            field_type: field_def.field_type.into(),
150            description: field_def.description,
151        }
152    }
153}
154
155impl From<RbFieldDefinition> for CoreFieldDefinition {
156    fn from(field_def: RbFieldDefinition) -> Self {
157        CoreFieldDefinition {
158            index: field_def.index,
159            name: field_def.name,
160            field_type: field_def.field_type.into(),
161            description: field_def.description,
162        }
163    }
164}
165
166impl From<FieldDefinition> for RbFieldDefinition {
167    fn from(field_def: FieldDefinition) -> Self {
168        RbFieldDefinition::from(CoreFieldDefinition::from(field_def))
169    }
170}
171
172impl From<RbFieldDefinition> for FieldDefinition {
173    fn from(field_def: RbFieldDefinition) -> Self {
174        FieldDefinition::from(CoreFieldDefinition::from(field_def))
175    }
176}
177
178/// Dictionary schema definition.
179///
180/// A thin magnus wrapper over [`lindera_binding_core::CoreSchema`], which owns
181/// the field storage, the name-to-index map, and the field lookups.
182#[magnus::wrap(class = "Lindera::Schema", free_immediately, size)]
183#[derive(Debug, Clone)]
184pub struct RbSchema {
185    /// The backing binding-core schema.
186    inner: CoreSchema,
187}
188
189impl RbSchema {
190    /// Creates a new `RbSchema` from a list of field names.
191    ///
192    /// # Arguments
193    ///
194    /// * `fields` - List of field names.
195    ///
196    /// # Returns
197    ///
198    /// A new `RbSchema` instance.
199    fn new(fields: Vec<String>) -> Self {
200        Self {
201            inner: CoreSchema::new(fields),
202        }
203    }
204
205    /// Creates a default schema for IPADIC-style dictionaries.
206    ///
207    /// # Returns
208    ///
209    /// A new `RbSchema` with default IPADIC fields.
210    fn create_default() -> Self {
211        Self {
212            inner: CoreSchema::create_default(),
213        }
214    }
215
216    /// Returns the list of field names as a Ruby array.
217    fn fields(&self) -> Vec<String> {
218        self.inner.fields().to_vec()
219    }
220
221    /// Returns the index of the specified field name.
222    ///
223    /// # Arguments
224    ///
225    /// * `field_name` - Name of the field.
226    ///
227    /// # Returns
228    ///
229    /// The index of the field, or None if not found.
230    fn get_field_index(&self, field_name: String) -> Option<usize> {
231        self.inner.get_field_index(&field_name)
232    }
233
234    /// Returns the number of fields in the schema.
235    ///
236    /// # Returns
237    ///
238    /// The number of fields.
239    fn field_count(&self) -> usize {
240        self.inner.field_count()
241    }
242
243    /// Returns the name of the field at the specified index.
244    ///
245    /// # Arguments
246    ///
247    /// * `index` - Index of the field.
248    ///
249    /// # Returns
250    ///
251    /// The field name, or None if the index is out of bounds.
252    fn get_field_name(&self, index: usize) -> Option<String> {
253        self.inner.get_field_name(index).map(str::to_string)
254    }
255
256    /// Returns the custom fields (fields after the first 4 standard fields).
257    ///
258    /// # Returns
259    ///
260    /// A list of custom field names.
261    fn get_custom_fields(&self) -> Vec<String> {
262        self.inner.get_custom_fields().to_vec()
263    }
264
265    /// Returns all field names.
266    ///
267    /// # Returns
268    ///
269    /// A list of all field names.
270    fn get_all_fields(&self) -> Vec<String> {
271        self.inner.fields().to_vec()
272    }
273
274    /// Returns the field definition for the specified field name.
275    ///
276    /// # Arguments
277    ///
278    /// * `name` - Name of the field.
279    ///
280    /// # Returns
281    ///
282    /// The field definition, or None if not found.
283    fn get_field_by_name(&self, name: String) -> Option<RbFieldDefinition> {
284        self.inner
285            .get_field_by_name(&name)
286            .map(RbFieldDefinition::from)
287    }
288
289    /// Validates a CSV record against the schema.
290    ///
291    /// # Arguments
292    ///
293    /// * `record` - List of field values.
294    ///
295    /// # Returns
296    ///
297    /// `Ok(())` if valid, or an error if the record is invalid.
298    fn validate_record(&self, record: RArray) -> Result<(), Error> {
299        let ruby = Ruby::get().expect("Ruby runtime not initialized");
300        let values: Vec<String> = record.to_vec()?;
301
302        self.inner
303            .validate_record(&values)
304            .map_err(|err| Error::new(ruby.exception_arg_error(), err.to_string()))
305    }
306
307    /// Returns the string representation of the schema.
308    fn to_s(&self) -> String {
309        format!("Schema(fields={})", self.inner.field_count())
310    }
311
312    /// Returns the inspect representation of the schema.
313    fn inspect(&self) -> String {
314        format!("#<Lindera::Schema: fields={:?}>", self.inner.fields())
315    }
316}
317
318impl RbSchema {
319    /// Internal constructor for use from other modules (not exposed to Ruby).
320    pub fn new_internal(fields: Vec<String>) -> Self {
321        Self::new(fields)
322    }
323
324    /// Internal default constructor for use from other modules (not exposed to Ruby).
325    pub fn create_default_internal() -> Self {
326        Self::create_default()
327    }
328}
329
330impl From<CoreSchema> for RbSchema {
331    fn from(schema: CoreSchema) -> Self {
332        RbSchema { inner: schema }
333    }
334}
335
336impl From<RbSchema> for CoreSchema {
337    fn from(schema: RbSchema) -> Self {
338        schema.inner
339    }
340}
341
342impl From<RbSchema> for Schema {
343    fn from(schema: RbSchema) -> Self {
344        schema.inner.into()
345    }
346}
347
348impl From<Schema> for RbSchema {
349    fn from(schema: Schema) -> Self {
350        RbSchema {
351            inner: CoreSchema::from(schema),
352        }
353    }
354}
355
356/// Defines Schema, FieldDefinition, and FieldType classes in the given Ruby module.
357///
358/// # Arguments
359///
360/// * `ruby` - Ruby runtime handle.
361/// * `module` - Parent Ruby module.
362///
363/// # Returns
364///
365/// `Ok(())` on success, or a Magnus `Error` on failure.
366pub fn define(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
367    let field_type_class = module.define_class("FieldType", ruby.class_object())?;
368    field_type_class.define_method("to_s", method!(RbFieldType::to_s, 0))?;
369    field_type_class.define_method("inspect", method!(RbFieldType::inspect, 0))?;
370
371    let field_def_class = module.define_class("FieldDefinition", ruby.class_object())?;
372    field_def_class.define_method("index", method!(RbFieldDefinition::index, 0))?;
373    field_def_class.define_method("name", method!(RbFieldDefinition::name, 0))?;
374    field_def_class.define_method("field_type", method!(RbFieldDefinition::field_type, 0))?;
375    field_def_class.define_method("description", method!(RbFieldDefinition::description, 0))?;
376    field_def_class.define_method("to_s", method!(RbFieldDefinition::to_s, 0))?;
377    field_def_class.define_method("inspect", method!(RbFieldDefinition::inspect, 0))?;
378
379    let schema_class = module.define_class("Schema", ruby.class_object())?;
380    schema_class.define_singleton_method("new", function!(RbSchema::new, 1))?;
381    schema_class
382        .define_singleton_method("create_default", function!(RbSchema::create_default, 0))?;
383    schema_class.define_method("fields", method!(RbSchema::fields, 0))?;
384    schema_class.define_method("get_field_index", method!(RbSchema::get_field_index, 1))?;
385    schema_class.define_method("field_count", method!(RbSchema::field_count, 0))?;
386    schema_class.define_method("get_field_name", method!(RbSchema::get_field_name, 1))?;
387    schema_class.define_method("get_custom_fields", method!(RbSchema::get_custom_fields, 0))?;
388    schema_class.define_method("get_all_fields", method!(RbSchema::get_all_fields, 0))?;
389    schema_class.define_method("get_field_by_name", method!(RbSchema::get_field_by_name, 1))?;
390    schema_class.define_method("validate_record", method!(RbSchema::validate_record, 1))?;
391    schema_class.define_method("to_s", method!(RbSchema::to_s, 0))?;
392    schema_class.define_method("inspect", method!(RbSchema::inspect, 0))?;
393
394    Ok(())
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_rb_field_type_surface_to_lindera() {
403        let rb = RbFieldType {
404            inner: RbFieldTypeKind::Surface,
405        };
406        let lindera: FieldType = rb.into();
407        assert!(matches!(lindera, FieldType::Surface));
408    }
409
410    #[test]
411    fn test_rb_field_type_custom_to_lindera() {
412        let rb = RbFieldType {
413            inner: RbFieldTypeKind::Custom,
414        };
415        let lindera: FieldType = rb.into();
416        assert!(matches!(lindera, FieldType::Custom));
417    }
418
419    #[test]
420    fn test_lindera_field_type_surface_to_rb() {
421        let rb: RbFieldType = FieldType::Surface.into();
422        assert!(matches!(rb.inner, RbFieldTypeKind::Surface));
423    }
424
425    #[test]
426    fn test_lindera_field_type_custom_to_rb() {
427        let rb: RbFieldType = FieldType::Custom.into();
428        assert!(matches!(rb.inner, RbFieldTypeKind::Custom));
429    }
430
431    #[test]
432    fn test_rb_schema_new_builds_index_map() {
433        let fields = vec!["a".to_string(), "b".to_string(), "c".to_string()];
434        let schema = RbSchema::new_internal(fields);
435        assert_eq!(schema.get_field_index("a".to_string()), Some(0));
436        assert_eq!(schema.get_field_index("b".to_string()), Some(1));
437        assert_eq!(schema.get_field_index("c".to_string()), Some(2));
438        assert_eq!(schema.get_field_index("d".to_string()), None);
439    }
440
441    #[test]
442    fn test_rb_schema_field_count() {
443        let fields = vec!["x".to_string(), "y".to_string()];
444        let schema = RbSchema::new_internal(fields);
445        assert_eq!(schema.field_count(), 2);
446    }
447
448    #[test]
449    fn test_rb_schema_get_custom_fields_with_more_than_4() {
450        let fields = vec![
451            "surface".to_string(),
452            "left_context_id".to_string(),
453            "right_context_id".to_string(),
454            "cost".to_string(),
455            "major_pos".to_string(),
456            "reading".to_string(),
457        ];
458        let schema = RbSchema::new_internal(fields);
459        let custom = schema.get_custom_fields();
460        assert_eq!(custom, vec!["major_pos", "reading"]);
461    }
462
463    #[test]
464    fn test_rb_schema_get_custom_fields_with_4_or_fewer() {
465        let fields = vec![
466            "surface".to_string(),
467            "left_context_id".to_string(),
468            "right_context_id".to_string(),
469            "cost".to_string(),
470        ];
471        let schema = RbSchema::new_internal(fields);
472        assert!(schema.get_custom_fields().is_empty());
473    }
474
475    #[test]
476    fn test_rb_schema_create_default() {
477        let schema = RbSchema::create_default_internal();
478        assert_eq!(schema.field_count(), 13);
479        assert_eq!(schema.fields()[0], "surface");
480        assert_eq!(schema.fields()[3], "cost");
481        assert_eq!(schema.fields()[5], "pos_detail_1");
482    }
483
484    #[test]
485    fn test_rb_schema_get_field_by_name() {
486        let schema = RbSchema::create_default_internal();
487        let surface = schema.get_field_by_name("surface".to_string()).unwrap();
488        assert_eq!(surface.index, 0);
489        assert!(matches!(surface.field_type.inner, RbFieldTypeKind::Surface));
490
491        let custom = schema
492            .get_field_by_name("pos_detail_1".to_string())
493            .unwrap();
494        assert_eq!(custom.index, 5);
495        assert!(matches!(custom.field_type.inner, RbFieldTypeKind::Custom));
496
497        assert!(schema.get_field_by_name("nope".to_string()).is_none());
498    }
499
500    #[test]
501    fn test_rb_schema_to_lindera_schema() {
502        let fields = vec!["a".to_string(), "b".to_string(), "c".to_string()];
503        let rb_schema = RbSchema::new_internal(fields.clone());
504        let lindera_schema: Schema = rb_schema.into();
505        assert_eq!(lindera_schema.get_all_fields(), &fields);
506    }
507
508    #[test]
509    fn test_lindera_schema_to_rb_schema() {
510        let fields = vec!["x".to_string(), "y".to_string(), "z".to_string()];
511        let lindera_schema = Schema::new(fields.clone());
512        let rb_schema: RbSchema = lindera_schema.into();
513        assert_eq!(rb_schema.fields(), fields);
514        assert_eq!(rb_schema.get_field_index("x".to_string()), Some(0));
515        assert_eq!(rb_schema.get_field_index("z".to_string()), Some(2));
516    }
517
518    #[test]
519    fn test_rb_schema_roundtrip() {
520        let fields = vec![
521            "surface".to_string(),
522            "left_context_id".to_string(),
523            "right_context_id".to_string(),
524            "cost".to_string(),
525            "reading".to_string(),
526        ];
527        let rb_schema = RbSchema::new_internal(fields.clone());
528        let lindera_schema: Schema = rb_schema.into();
529        let back: RbSchema = lindera_schema.into();
530        assert_eq!(back.fields(), fields);
531        assert_eq!(back.field_count(), 5);
532    }
533
534    #[test]
535    fn test_rb_field_definition_to_lindera() {
536        let rb_def = RbFieldDefinition {
537            index: 2,
538            name: "right_context_id".to_string(),
539            field_type: RbFieldType {
540                inner: RbFieldTypeKind::RightContextId,
541            },
542            description: Some("Right context ID".to_string()),
543        };
544        let lindera_def: FieldDefinition = rb_def.into();
545        assert_eq!(lindera_def.index, 2);
546        assert_eq!(lindera_def.name, "right_context_id");
547        assert!(matches!(lindera_def.field_type, FieldType::RightContextId));
548        assert_eq!(
549            lindera_def.description,
550            Some("Right context ID".to_string())
551        );
552    }
553
554    #[test]
555    fn test_lindera_field_definition_to_rb() {
556        let lindera_def = FieldDefinition {
557            index: 4,
558            name: "major_pos".to_string(),
559            field_type: FieldType::Custom,
560            description: None,
561        };
562        let rb_def: RbFieldDefinition = lindera_def.into();
563        assert_eq!(rb_def.index, 4);
564        assert_eq!(rb_def.name, "major_pos");
565        assert!(matches!(rb_def.field_type.inner, RbFieldTypeKind::Custom));
566        assert!(rb_def.description.is_none());
567    }
568}