1use lindera::dictionary::{FieldDefinition, FieldType, Schema};
8
9use crate::error::{CoreError, CoreResult};
10
11pub fn default_dictionary_fields() -> Vec<String> {
19 [
20 "surface",
21 "left_context_id",
22 "right_context_id",
23 "cost",
24 "major_pos",
25 "pos_detail_1",
26 "pos_detail_2",
27 "pos_detail_3",
28 "conjugation_type",
29 "conjugation_form",
30 "base_form",
31 "reading",
32 "pronunciation",
33 ]
34 .into_iter()
35 .map(String::from)
36 .collect()
37}
38
39pub fn validate_record(fields: &[String], record: &[String]) -> Result<(), String> {
45 if record.len() < fields.len() {
46 return Err(format!(
47 "CSV row has {} fields but schema requires {} fields",
48 record.len(),
49 fields.len()
50 ));
51 }
52
53 for (index, field_name) in fields.iter().enumerate() {
54 if index < record.len() && record[index].trim().is_empty() {
55 return Err(format!("Field {field_name} is missing or empty"));
56 }
57 }
58
59 Ok(())
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CoreFieldType {
66 Surface,
68 LeftContextId,
70 RightContextId,
72 Cost,
74 Custom,
76}
77
78impl CoreFieldType {
79 pub fn as_str(&self) -> &'static str {
81 match self {
82 CoreFieldType::Surface => "surface",
83 CoreFieldType::LeftContextId => "left_context_id",
84 CoreFieldType::RightContextId => "right_context_id",
85 CoreFieldType::Cost => "cost",
86 CoreFieldType::Custom => "custom",
87 }
88 }
89}
90
91impl From<FieldType> for CoreFieldType {
92 fn from(field_type: FieldType) -> Self {
94 match field_type {
95 FieldType::Surface => CoreFieldType::Surface,
96 FieldType::LeftContextId => CoreFieldType::LeftContextId,
97 FieldType::RightContextId => CoreFieldType::RightContextId,
98 FieldType::Cost => CoreFieldType::Cost,
99 FieldType::Custom => CoreFieldType::Custom,
100 }
101 }
102}
103
104impl From<CoreFieldType> for FieldType {
105 fn from(field_type: CoreFieldType) -> Self {
107 match field_type {
108 CoreFieldType::Surface => FieldType::Surface,
109 CoreFieldType::LeftContextId => FieldType::LeftContextId,
110 CoreFieldType::RightContextId => FieldType::RightContextId,
111 CoreFieldType::Cost => FieldType::Cost,
112 CoreFieldType::Custom => FieldType::Custom,
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct CoreFieldDefinition {
121 pub index: usize,
123 pub name: String,
125 pub field_type: CoreFieldType,
127 pub description: Option<String>,
129}
130
131impl From<FieldDefinition> for CoreFieldDefinition {
132 fn from(field_def: FieldDefinition) -> Self {
134 CoreFieldDefinition {
135 index: field_def.index,
136 name: field_def.name,
137 field_type: field_def.field_type.into(),
138 description: field_def.description,
139 }
140 }
141}
142
143impl From<CoreFieldDefinition> for FieldDefinition {
144 fn from(field_def: CoreFieldDefinition) -> Self {
146 FieldDefinition {
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
155#[derive(Debug, Clone)]
161pub struct CoreSchema {
162 inner: Schema,
164}
165
166impl CoreSchema {
167 pub fn new(fields: Vec<String>) -> Self {
169 Self {
170 inner: Schema::new(fields),
171 }
172 }
173
174 pub fn create_default() -> Self {
176 Self::new(default_dictionary_fields())
177 }
178
179 pub fn fields(&self) -> &[String] {
181 self.inner.get_all_fields()
182 }
183
184 pub fn get_field_index(&self, field_name: &str) -> Option<usize> {
186 self.inner.get_field_index(field_name)
187 }
188
189 pub fn field_count(&self) -> usize {
191 self.inner.field_count()
192 }
193
194 pub fn get_field_name(&self, index: usize) -> Option<&str> {
196 self.inner.get_field_name(index)
197 }
198
199 pub fn get_custom_fields(&self) -> &[String] {
201 self.inner.get_custom_fields()
202 }
203
204 pub fn get_field_by_name(&self, name: &str) -> Option<CoreFieldDefinition> {
206 self.inner
207 .get_field_by_name(name)
208 .map(CoreFieldDefinition::from)
209 }
210
211 pub fn validate_record(&self, record: &[String]) -> CoreResult<()> {
216 validate_record(self.fields(), record).map_err(CoreError::validation)
217 }
218
219 pub fn as_lindera(&self) -> &Schema {
221 &self.inner
222 }
223
224 pub fn into_lindera(self) -> Schema {
226 self.inner
227 }
228}
229
230impl From<Schema> for CoreSchema {
231 fn from(schema: Schema) -> Self {
233 Self { inner: schema }
234 }
235}
236
237impl From<CoreSchema> for Schema {
238 fn from(schema: CoreSchema) -> Self {
240 schema.inner
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn default_fields_count() {
250 assert_eq!(default_dictionary_fields().len(), 13);
251 assert_eq!(default_dictionary_fields()[0], "surface");
252 assert_eq!(default_dictionary_fields()[5], "pos_detail_1");
253 }
254
255 #[test]
256 fn validate_ok() {
257 let fields = default_dictionary_fields();
258 let record: Vec<String> = (0..13).map(|i| format!("v{i}")).collect();
259 assert!(validate_record(&fields, &record).is_ok());
260 }
261
262 #[test]
263 fn validate_too_few_fields() {
264 let fields = default_dictionary_fields();
265 let record = vec!["a".to_string(), "b".to_string()];
266 let err = validate_record(&fields, &record).unwrap_err();
267 assert!(err.contains("requires 13 fields"));
268 }
269
270 #[test]
271 fn validate_empty_field() {
272 let fields = vec!["surface".to_string(), "reading".to_string()];
273 let record = vec!["x".to_string(), " ".to_string()];
274 let err = validate_record(&fields, &record).unwrap_err();
275 assert!(err.contains("reading"));
276 }
277
278 #[test]
279 fn core_field_type_roundtrips_with_lindera() {
280 for ft in [
281 FieldType::Surface,
282 FieldType::LeftContextId,
283 FieldType::RightContextId,
284 FieldType::Cost,
285 FieldType::Custom,
286 ] {
287 let core: CoreFieldType = ft.clone().into();
288 let back: FieldType = core.into();
289 assert_eq!(back, ft);
290 }
291 assert_eq!(CoreFieldType::Surface.as_str(), "surface");
292 assert_eq!(CoreFieldType::Custom.as_str(), "custom");
293 }
294
295 #[test]
296 fn core_field_definition_roundtrips_with_lindera() {
297 let fd = FieldDefinition {
298 index: 4,
299 name: "major_pos".to_string(),
300 field_type: FieldType::Custom,
301 description: None,
302 };
303 let core: CoreFieldDefinition = fd.clone().into();
304 assert_eq!(core.index, 4);
305 assert_eq!(core.name, "major_pos");
306 assert_eq!(core.field_type, CoreFieldType::Custom);
307 let back: FieldDefinition = core.into();
308 assert_eq!(back.index, fd.index);
309 assert_eq!(back.name, fd.name);
310 }
311
312 #[test]
313 fn core_schema_default_matches_lindera() {
314 let schema = CoreSchema::create_default();
315 assert_eq!(schema.field_count(), 13);
316 assert_eq!(schema.fields()[0], "surface");
317 assert_eq!(schema.fields()[5], "pos_detail_1");
320 assert_eq!(schema.fields()[12], "pronunciation");
321 assert_eq!(
322 schema.fields(),
323 lindera::dictionary::Schema::default().get_all_fields()
324 );
325 }
326
327 #[test]
328 fn core_schema_lookups() {
329 let schema = CoreSchema::new(vec![
330 "surface".to_string(),
331 "left_context_id".to_string(),
332 "right_context_id".to_string(),
333 "cost".to_string(),
334 "major_pos".to_string(),
335 "reading".to_string(),
336 ]);
337 assert_eq!(schema.get_field_index("surface"), Some(0));
338 assert_eq!(schema.get_field_index("reading"), Some(5));
339 assert_eq!(schema.get_field_index("nonexistent"), None);
340 assert_eq!(schema.get_field_name(4), Some("major_pos"));
341 assert_eq!(schema.get_field_name(99), None);
342 assert_eq!(schema.get_custom_fields(), ["major_pos", "reading"]);
343 }
344
345 #[test]
346 fn core_schema_get_field_by_name_classifies_type() {
347 let schema = CoreSchema::create_default();
348 let surface = schema.get_field_by_name("surface").unwrap();
349 assert_eq!(surface.index, 0);
350 assert_eq!(surface.field_type, CoreFieldType::Surface);
351
352 let custom = schema.get_field_by_name("major_pos").unwrap();
353 assert_eq!(custom.index, 4);
354 assert_eq!(custom.field_type, CoreFieldType::Custom);
355
356 assert!(schema.get_field_by_name("nonexistent").is_none());
357 }
358
359 #[test]
360 fn core_schema_validate_record() {
361 let schema = CoreSchema::new(vec!["surface".to_string(), "reading".to_string()]);
362 assert!(
363 schema
364 .validate_record(&["x".to_string(), "y".to_string()])
365 .is_ok()
366 );
367 let err = schema.validate_record(&["x".to_string()]).unwrap_err();
368 assert_eq!(err.kind(), crate::ErrorKind::Validation);
369 }
370
371 #[test]
372 fn core_schema_converts_to_and_from_lindera() {
373 let schema = CoreSchema::new(vec!["surface".to_string(), "pos".to_string()]);
374 let lindera: Schema = schema.clone().into();
375 assert_eq!(lindera.get_all_fields().len(), 2);
376 let back: CoreSchema = lindera.into();
377 assert_eq!(back.fields(), schema.fields());
378 }
379}