use lindera::dictionary::{FieldDefinition, FieldType, Schema};
use crate::error::{CoreError, CoreResult};
pub fn default_dictionary_fields() -> Vec<String> {
[
"surface",
"left_context_id",
"right_context_id",
"cost",
"major_pos",
"pos_detail_1",
"pos_detail_2",
"pos_detail_3",
"conjugation_type",
"conjugation_form",
"base_form",
"reading",
"pronunciation",
]
.into_iter()
.map(String::from)
.collect()
}
pub fn validate_record(fields: &[String], record: &[String]) -> Result<(), String> {
if record.len() < fields.len() {
return Err(format!(
"CSV row has {} fields but schema requires {} fields",
record.len(),
fields.len()
));
}
for (index, field_name) in fields.iter().enumerate() {
if index < record.len() && record[index].trim().is_empty() {
return Err(format!("Field {field_name} is missing or empty"));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoreFieldType {
Surface,
LeftContextId,
RightContextId,
Cost,
Custom,
}
impl CoreFieldType {
pub fn as_str(&self) -> &'static str {
match self {
CoreFieldType::Surface => "surface",
CoreFieldType::LeftContextId => "left_context_id",
CoreFieldType::RightContextId => "right_context_id",
CoreFieldType::Cost => "cost",
CoreFieldType::Custom => "custom",
}
}
}
impl From<FieldType> for CoreFieldType {
fn from(field_type: FieldType) -> Self {
match field_type {
FieldType::Surface => CoreFieldType::Surface,
FieldType::LeftContextId => CoreFieldType::LeftContextId,
FieldType::RightContextId => CoreFieldType::RightContextId,
FieldType::Cost => CoreFieldType::Cost,
FieldType::Custom => CoreFieldType::Custom,
}
}
}
impl From<CoreFieldType> for FieldType {
fn from(field_type: CoreFieldType) -> Self {
match field_type {
CoreFieldType::Surface => FieldType::Surface,
CoreFieldType::LeftContextId => FieldType::LeftContextId,
CoreFieldType::RightContextId => FieldType::RightContextId,
CoreFieldType::Cost => FieldType::Cost,
CoreFieldType::Custom => FieldType::Custom,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoreFieldDefinition {
pub index: usize,
pub name: String,
pub field_type: CoreFieldType,
pub description: Option<String>,
}
impl From<FieldDefinition> for CoreFieldDefinition {
fn from(field_def: FieldDefinition) -> Self {
CoreFieldDefinition {
index: field_def.index,
name: field_def.name,
field_type: field_def.field_type.into(),
description: field_def.description,
}
}
}
impl From<CoreFieldDefinition> for FieldDefinition {
fn from(field_def: CoreFieldDefinition) -> Self {
FieldDefinition {
index: field_def.index,
name: field_def.name,
field_type: field_def.field_type.into(),
description: field_def.description,
}
}
}
#[derive(Debug, Clone)]
pub struct CoreSchema {
inner: Schema,
}
impl CoreSchema {
pub fn new(fields: Vec<String>) -> Self {
Self {
inner: Schema::new(fields),
}
}
pub fn create_default() -> Self {
Self::new(default_dictionary_fields())
}
pub fn fields(&self) -> &[String] {
self.inner.get_all_fields()
}
pub fn get_field_index(&self, field_name: &str) -> Option<usize> {
self.inner.get_field_index(field_name)
}
pub fn field_count(&self) -> usize {
self.inner.field_count()
}
pub fn get_field_name(&self, index: usize) -> Option<&str> {
self.inner.get_field_name(index)
}
pub fn get_custom_fields(&self) -> &[String] {
self.inner.get_custom_fields()
}
pub fn get_field_by_name(&self, name: &str) -> Option<CoreFieldDefinition> {
self.inner
.get_field_by_name(name)
.map(CoreFieldDefinition::from)
}
pub fn validate_record(&self, record: &[String]) -> CoreResult<()> {
validate_record(self.fields(), record).map_err(CoreError::validation)
}
pub fn as_lindera(&self) -> &Schema {
&self.inner
}
pub fn into_lindera(self) -> Schema {
self.inner
}
}
impl From<Schema> for CoreSchema {
fn from(schema: Schema) -> Self {
Self { inner: schema }
}
}
impl From<CoreSchema> for Schema {
fn from(schema: CoreSchema) -> Self {
schema.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_fields_count() {
assert_eq!(default_dictionary_fields().len(), 13);
assert_eq!(default_dictionary_fields()[0], "surface");
assert_eq!(default_dictionary_fields()[5], "pos_detail_1");
}
#[test]
fn validate_ok() {
let fields = default_dictionary_fields();
let record: Vec<String> = (0..13).map(|i| format!("v{i}")).collect();
assert!(validate_record(&fields, &record).is_ok());
}
#[test]
fn validate_too_few_fields() {
let fields = default_dictionary_fields();
let record = vec!["a".to_string(), "b".to_string()];
let err = validate_record(&fields, &record).unwrap_err();
assert!(err.contains("requires 13 fields"));
}
#[test]
fn validate_empty_field() {
let fields = vec!["surface".to_string(), "reading".to_string()];
let record = vec!["x".to_string(), " ".to_string()];
let err = validate_record(&fields, &record).unwrap_err();
assert!(err.contains("reading"));
}
#[test]
fn core_field_type_roundtrips_with_lindera() {
for ft in [
FieldType::Surface,
FieldType::LeftContextId,
FieldType::RightContextId,
FieldType::Cost,
FieldType::Custom,
] {
let core: CoreFieldType = ft.clone().into();
let back: FieldType = core.into();
assert_eq!(back, ft);
}
assert_eq!(CoreFieldType::Surface.as_str(), "surface");
assert_eq!(CoreFieldType::Custom.as_str(), "custom");
}
#[test]
fn core_field_definition_roundtrips_with_lindera() {
let fd = FieldDefinition {
index: 4,
name: "major_pos".to_string(),
field_type: FieldType::Custom,
description: None,
};
let core: CoreFieldDefinition = fd.clone().into();
assert_eq!(core.index, 4);
assert_eq!(core.name, "major_pos");
assert_eq!(core.field_type, CoreFieldType::Custom);
let back: FieldDefinition = core.into();
assert_eq!(back.index, fd.index);
assert_eq!(back.name, fd.name);
}
#[test]
fn core_schema_default_matches_lindera() {
let schema = CoreSchema::create_default();
assert_eq!(schema.field_count(), 13);
assert_eq!(schema.fields()[0], "surface");
assert_eq!(schema.fields()[5], "pos_detail_1");
assert_eq!(schema.fields()[12], "pronunciation");
assert_eq!(
schema.fields(),
lindera::dictionary::Schema::default().get_all_fields()
);
}
#[test]
fn core_schema_lookups() {
let schema = CoreSchema::new(vec![
"surface".to_string(),
"left_context_id".to_string(),
"right_context_id".to_string(),
"cost".to_string(),
"major_pos".to_string(),
"reading".to_string(),
]);
assert_eq!(schema.get_field_index("surface"), Some(0));
assert_eq!(schema.get_field_index("reading"), Some(5));
assert_eq!(schema.get_field_index("nonexistent"), None);
assert_eq!(schema.get_field_name(4), Some("major_pos"));
assert_eq!(schema.get_field_name(99), None);
assert_eq!(schema.get_custom_fields(), ["major_pos", "reading"]);
}
#[test]
fn core_schema_get_field_by_name_classifies_type() {
let schema = CoreSchema::create_default();
let surface = schema.get_field_by_name("surface").unwrap();
assert_eq!(surface.index, 0);
assert_eq!(surface.field_type, CoreFieldType::Surface);
let custom = schema.get_field_by_name("major_pos").unwrap();
assert_eq!(custom.index, 4);
assert_eq!(custom.field_type, CoreFieldType::Custom);
assert!(schema.get_field_by_name("nonexistent").is_none());
}
#[test]
fn core_schema_validate_record() {
let schema = CoreSchema::new(vec!["surface".to_string(), "reading".to_string()]);
assert!(
schema
.validate_record(&["x".to_string(), "y".to_string()])
.is_ok()
);
let err = schema.validate_record(&["x".to_string()]).unwrap_err();
assert_eq!(err.kind(), crate::ErrorKind::Validation);
}
#[test]
fn core_schema_converts_to_and_from_lindera() {
let schema = CoreSchema::new(vec!["surface".to_string(), "pos".to_string()]);
let lindera: Schema = schema.clone().into();
assert_eq!(lindera.get_all_fields().len(), 2);
let back: CoreSchema = lindera.into();
assert_eq!(back.fields(), schema.fields());
}
}