keyset 0.2.0

A Rust tool to create pretty keyset layout diagrams using correct fonts and icons.
Documentation
use std::fmt;

use crate::kle::InvalidKleLayout;

#[derive(Debug)]
pub struct Error {
    inner: Box<ErrorImpl>,
}

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug)]
pub(crate) enum ErrorImpl {
    JsonParseError(serde_json::Error),
    TomlParseError(toml::de::Error),
    FontParseError(ttf_parser::FaceParsingError),
    InvalidKleLayout(InvalidKleLayout),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self.inner {
            ErrorImpl::JsonParseError(error) => write!(f, "error parsing JSON: {error}"),
            ErrorImpl::TomlParseError(error) => write!(f, "error parsing TOML: {error}"),
            ErrorImpl::FontParseError(error) => write!(f, "error parsing font: {error}"),
            ErrorImpl::InvalidKleLayout(error) => write!(f, "error parsing KLE layout: {error}"),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &*self.inner {
            ErrorImpl::JsonParseError(error) => Some(error),
            ErrorImpl::TomlParseError(error) => Some(error),
            ErrorImpl::FontParseError(error) => Some(error),
            ErrorImpl::InvalidKleLayout(_) => None,
        }
    }
}

impl From<serde_json::Error> for Error {
    fn from(error: serde_json::Error) -> Self {
        Self {
            inner: Box::new(ErrorImpl::JsonParseError(error)),
        }
    }
}

impl From<toml::de::Error> for Error {
    fn from(error: toml::de::Error) -> Self {
        Self {
            inner: Box::new(ErrorImpl::TomlParseError(error)),
        }
    }
}

impl From<ttf_parser::FaceParsingError> for Error {
    fn from(error: ttf_parser::FaceParsingError) -> Self {
        Self {
            inner: Box::new(ErrorImpl::FontParseError(error)),
        }
    }
}

impl From<InvalidKleLayout> for Error {
    fn from(error: InvalidKleLayout) -> Self {
        Self {
            inner: Box::new(ErrorImpl::InvalidKleLayout(error)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use unindent::unindent;

    use crate::kle;

    fn json_parse_error() -> Error {
        serde_json::from_str::<serde_json::Value>("invalid")
            .unwrap_err()
            .into()
    }

    fn toml_parse_error() -> Error {
        toml::from_str::<toml::Value>("invalid").unwrap_err().into()
    }

    fn font_parse_error() -> Error {
        ttf_parser::Face::parse(b"invalid", 0).unwrap_err().into()
    }

    fn invalid_key_size() -> Error {
        let kle = r#"[[{"w": 1, "h": 1, "x2": 1, "y2": 1, "w2": 1, "h2": 1}, "A"]]"#;
        kle::from_json(kle).unwrap_err()
    }

    #[test]
    fn test_display_error() {
        let config = vec![
            (
                json_parse_error(),
                "error parsing JSON: expected value at line 1 column 1".to_owned(),
            ),
            (
                toml_parse_error(),
                unindent(
                    r#"error parsing TOML: TOML parse error at line 1, column 8
                      |
                    1 | invalid
                      |        ^
                    expected `.`, `=`
                    "#,
                ),
            ),
            (
                font_parse_error(),
                "error parsing font: unknown magic".to_owned(),
            ),
            (
                invalid_key_size(),
                "error parsing KLE layout: Unsupported non-standard key size \
                (w: 1.00, h: 1.00, x2: 1.00, y2: 1.00, w2: 1.00, h2: 1.00). \
                Note only ISO enter and stepped caps are supported as special cases"
                    .to_owned(),
            ),
        ];

        for (err, fmt) in config {
            assert_eq!(format!("{err}"), fmt);
        }
    }

    #[test]
    fn test_error_source() {
        use std::error::Error;

        let config = vec![
            (json_parse_error(), true),
            (toml_parse_error(), true),
            (font_parse_error(), true),
            (invalid_key_size(), false),
        ];

        for (err, has_source) in config {
            assert_eq!(err.source().is_some(), has_source);
        }
    }
}