rshogi-usi 0.1.5

Engine-agnostic USI protocol command model, parser, and formatter
Documentation
use std::error::Error;
use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CanonicalTokenMismatch {
    pub token_position: usize,
    pub expected: Option<String>,
    pub found: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseErrorSite {
    pub token_position: usize,
    pub byte_start: usize,
    pub byte_end: usize,
    pub token: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseErrorKind {
    EmptyInput,
    UnknownCommand { command: String },
    MissingArgument { context: &'static str },
    InvalidValue { field: &'static str, value: String },
    UnexpectedToken { context: &'static str, token: String },
    NonCanonical { canonical: String },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
    kind: Box<ParseErrorKind>,
    canonical_token_mismatch: Option<Box<CanonicalTokenMismatch>>,
    site: Option<Box<ParseErrorSite>>,
}

impl ParseError {
    #[must_use]
    pub fn new(kind: ParseErrorKind) -> Self {
        Self { kind: Box::new(kind), canonical_token_mismatch: None, site: None }
    }

    #[must_use]
    pub fn with_canonical_token_mismatch(mut self, mismatch: CanonicalTokenMismatch) -> Self {
        self.canonical_token_mismatch = Some(Box::new(mismatch));
        self
    }

    #[must_use]
    pub fn with_site(mut self, site: ParseErrorSite) -> Self {
        self.site = Some(Box::new(site));
        self
    }

    #[must_use]
    pub fn kind(&self) -> &ParseErrorKind {
        self.kind.as_ref()
    }

    #[must_use]
    pub fn canonical_token_mismatch(&self) -> Option<&CanonicalTokenMismatch> {
        self.canonical_token_mismatch.as_deref()
    }

    #[must_use]
    pub fn site(&self) -> Option<&ParseErrorSite> {
        self.site.as_deref()
    }
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind.as_ref() {
            ParseErrorKind::EmptyInput => f.write_str("empty command"),
            ParseErrorKind::UnknownCommand { command } => {
                write!(f, "unknown command `{command}`")
            }
            ParseErrorKind::MissingArgument { context } => write!(f, "missing argument: {context}"),
            ParseErrorKind::InvalidValue { field, value } => {
                write!(f, "invalid {field}: `{value}`")
            }
            ParseErrorKind::UnexpectedToken { context, token } => {
                write!(f, "unexpected token `{token}` while parsing {context}")
            }
            ParseErrorKind::NonCanonical { canonical } => {
                write!(f, "non-canonical command; expected `{canonical}`")?;
                if let Some(mismatch) = self.canonical_token_mismatch() {
                    write!(
                        f,
                        "; first token mismatch at position {}: expected {}, found {}",
                        mismatch.token_position,
                        describe_token(mismatch.expected.as_deref()),
                        describe_token(mismatch.found.as_deref())
                    )?;
                }
                Ok(())
            }
        }?;

        if let Some(site) = self.site() {
            write!(f, "; {}", describe_site(site))?;
        }

        Ok(())
    }
}

impl Error for ParseError {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortabilityErrorKind {
    WhitespaceInOptionName { context: &'static str, name: String },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortabilityError {
    kind: Box<PortabilityErrorKind>,
}

impl PortabilityError {
    #[must_use]
    pub fn new(kind: PortabilityErrorKind) -> Self {
        Self { kind: Box::new(kind) }
    }

    #[must_use]
    pub fn kind(&self) -> &PortabilityErrorKind {
        self.kind.as_ref()
    }
}

impl fmt::Display for PortabilityError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind.as_ref() {
            PortabilityErrorKind::WhitespaceInOptionName { context, name } => write!(
                f,
                "non-portable {context} option name `{name}`; GUI implementations such as ShogiHome expect a single token"
            ),
        }
    }
}

impl Error for PortabilityError {}

fn describe_token(token: Option<&str>) -> String {
    token.map_or_else(|| "end of input".to_string(), |token| format!("`{token}`"))
}

fn describe_site(site: &ParseErrorSite) -> String {
    site.token.as_deref().map_or_else(
        || {
            format!(
                "at end of input (token {}, bytes {}..{})",
                site.token_position, site.byte_start, site.byte_end
            )
        },
        |token| {
            format!(
                "at token {} (`{token}`) bytes {}..{}",
                site.token_position, site.byte_start, site.byte_end
            )
        },
    )
}