niho 0.2.0

A command-line tool for converting romanized Japanese text to Japanese characters
Documentation
use std::{borrow::Cow, num::NonZeroUsize};

#[derive(Debug)]
pub struct Dictionary<'a> {
    text: &'a str,
    line: usize,
}

impl<'a> Dictionary<'a> {
    pub const DEFAULT: &'static str = include_str!("../dictionaries/default.jsonl");

    pub fn new(text: &'a str) -> Self {
        Self { text, line: 0 }
    }
}

impl<'a> Iterator for Dictionary<'a> {
    type Item = Result<DictionaryEntry<'a>, DictionaryError>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.text.is_empty() {
            return None;
        }
        let (current, remaining) = self.text.split_once('\n').unwrap_or((self.text, ""));
        self.text = remaining;

        self.line += 1;
        DictionaryEntry::parse(current)
            .map(Some)
            .map_err(|e| DictionaryError {
                line: self.line,
                reason: e,
            })
            .transpose()
    }
}

pub type DictionaryString<'a> = Cow<'a, str>;

#[derive(Debug)]
pub enum DictionaryEntry<'a> {
    Hiragana {
        from: DictionaryString<'a>,
        to: DictionaryString<'a>,
        consume_chars: Option<NonZeroUsize>,
    },
    Katakana {
        from: DictionaryString<'a>,
        to: DictionaryString<'a>,
        consume_chars: Option<NonZeroUsize>,
    },
    Kanji {
        from: DictionaryString<'a>,
        to: Vec<DictionaryString<'a>>,
    },
}

impl<'a> DictionaryEntry<'a> {
    fn parse(line: &'a str) -> Result<Self, nojson::JsonParseError> {
        let json = nojson::RawJson::parse(line)?;
        let value = json.value();
        match value
            .to_member("type")?
            .required()?
            .to_unquoted_string_str()?
            .as_ref()
        {
            "hiragana" => Ok(Self::Hiragana {
                from: value
                    .to_member("from")?
                    .required()?
                    .to_unquoted_string_str()?,
                to: value
                    .to_member("to")?
                    .required()?
                    .to_unquoted_string_str()?,
                consume_chars: value.to_member("consume_chars")?.try_into()?,
            }),
            "katakana" => Ok(Self::Katakana {
                from: value
                    .to_member("from")?
                    .required()?
                    .to_unquoted_string_str()?,
                to: value
                    .to_member("to")?
                    .required()?
                    .to_unquoted_string_str()?,
                consume_chars: value.to_member("consume_chars")?.try_into()?,
            }),
            "kanji" => Ok(Self::Kanji {
                from: value
                    .to_member("from")?
                    .required()?
                    .to_unquoted_string_str()?,
                to: value.to_member("to")?.required()?.try_into()?,
            }),
            ty => Err(value.invalid(format!("unknown type: {ty:?}"))),
        }
    }
}

#[derive(Debug)]
pub struct DictionaryError {
    pub line: usize,
    pub reason: nojson::JsonParseError,
}

impl std::fmt::Display for DictionaryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Dictionary parse error at line {}: {}",
            self.line, self.reason
        )
    }
}

impl std::error::Error for DictionaryError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.reason)
    }
}