[explorer, moves, core] Improve error handling in explorer

Implement thiserror::Error for a bunch of error types, and remove string errors
from the implementation of the command handler in explorer.

Clean up parsing of basic types all over the place.

Update Cargo files to include thiserror and anyhow.
This commit is contained in:
Eryn Wells 2025-05-19 14:18:31 -07:00
parent 72eeba84ba
commit 9010f1e9c2
12 changed files with 331 additions and 226 deletions

View file

@ -1,7 +1,7 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{errors::TryFromCharError, try_from_string, Direction};
use std::fmt;
use crate::Direction;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum Color {
@ -48,31 +48,65 @@ impl Color {
pub const fn next(&self) -> Color {
Self::ALL[((*self as usize) + 1) % Self::NUM]
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Color::White => "white",
Color::Black => "black",
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Color::White => "White",
Color::Black => "Black",
Color::White => "white",
Color::Black => "black",
},
)
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching color for character '{0}'")]
pub struct ColorFromCharError(char);
impl TryFrom<char> for Color {
type Error = TryFromCharError;
type Error = ColorFromCharError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'w' | 'W' => Ok(Color::White),
'b' | 'B' => Ok(Color::Black),
_ => Err(TryFromCharError),
_ => Err(ColorFromCharError(value)),
}
}
}
try_from_string!(Color);
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching color for string")]
pub struct ColorFromStrError;
impl TryFrom<&str> for Color {
type Error = ColorFromStrError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"w" | "white" => Ok(Color::White),
"b" | "black" => Ok(Color::Black),
_ => Err(ColorFromStrError),
}
}
}
impl std::str::FromStr for Color {
type Err = ColorFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s.to_lowercase().as_str())
}
}

View file

@ -1,7 +1,7 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Color;
use std::{fmt, str::FromStr};
use std::fmt;
use thiserror::Error;
macro_rules! try_from_integer {
@ -10,6 +10,7 @@ macro_rules! try_from_integer {
type Error = ();
fn try_from(value: $int_type) -> Result<Self, Self::Error> {
#[allow(clippy::cast_possible_truncation)]
Self::try_from(value as u8)
}
}
@ -55,6 +56,7 @@ macro_rules! range_bound_struct {
#[allow(dead_code)]
impl $type {
$vis const NUM: usize = $max;
$vis const FIRST: $type = $type(0);
$vis const LAST: $type = $type($max - 1);
}
@ -148,7 +150,7 @@ impl File {
pub const G: File = File(6);
pub const H: File = File(7);
pub const ALL: [File; 8] = [
pub const ALL: [File; File::NUM] = [
File::A,
File::B,
File::C,
@ -173,7 +175,7 @@ impl Rank {
pub const SEVEN: Rank = Rank(6);
pub const EIGHT: Rank = Rank(7);
pub const ALL: [Rank; 8] = [
pub const ALL: [Rank; Self::NUM] = [
Rank::ONE,
Rank::TWO,
Rank::THREE,
@ -344,7 +346,27 @@ pub enum ParseSquareError {
FileError(#[from] ParseFileError),
}
impl FromStr for Square {
impl TryFrom<&str> for Square {
type Error = ParseSquareError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut chars = value.chars();
let file: File = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::FileError(ParseFileError))?;
let rank: Rank = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::RankError(ParseRankError))?;
Ok(Square::from_file_rank(file, rank))
}
}
impl std::str::FromStr for Square {
type Err = ParseSquareError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -377,10 +399,13 @@ impl std::str::FromStr for Rank {
.nth(0)
.ok_or(ParseRankError)
.map(|ch| ch.to_ascii_lowercase())?;
let offset = 'a' as usize - (ch as usize);
let rank = Rank::ALL[offset];
Ok(rank)
let offset = 'a' as usize - (ch as usize);
if offset >= Rank::ALL.len() {
return Err(ParseRankError);
}
Ok(Rank::ALL[offset])
}
}
@ -397,10 +422,13 @@ impl std::str::FromStr for File {
.nth(0)
.ok_or(ParseFileError)
.map(|ch| ch.to_ascii_lowercase())?;
let offset = '1' as usize - (ch as usize);
let file = File::ALL[offset];
Ok(file)
let offset = '1' as usize - (ch as usize);
if offset >= File::ALL.len() {
return Err(ParseFileError);
}
Ok(File::ALL[offset])
}
}
@ -489,12 +517,12 @@ mod tests {
#[test]
fn bad_algebraic_input() {
assert!(Square::from_algebraic_str("a0").is_err());
assert!(Square::from_algebraic_str("j3").is_err());
assert!(Square::from_algebraic_str("a11").is_err());
assert!(Square::from_algebraic_str("b-1").is_err());
assert!(Square::from_algebraic_str("a 1").is_err());
assert!(Square::from_algebraic_str("").is_err());
assert!("a0".parse::<Square>().is_err());
assert!("j3".parse::<Square>().is_err());
assert!("a11".parse::<Square>().is_err());
assert!("b-1".parse::<Square>().is_err());
assert!("a 1".parse::<Square>().is_err());
assert!("".parse::<Square>().is_err());
}
#[test]

View file

@ -1,7 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[derive(Debug, Eq, PartialEq)]
pub struct TryFromCharError;
#[derive(Debug, Eq, PartialEq)]
pub struct TryFromStrError;

View file

@ -2,11 +2,12 @@
pub mod colors;
pub mod coordinates;
pub mod errors;
pub mod pieces;
pub mod shapes;
mod macros;
pub use colors::Color;
pub use coordinates::{Direction, File, Rank, Square};
pub use pieces::{Piece, PlacedPiece, Shape};
pub use pieces::{Piece, PlacedPiece};
pub use shapes::Shape;

View file

@ -1,26 +1,5 @@
// Eryn Wells <eryn@erynwells.me>
#[macro_export]
macro_rules! try_from_string {
($type:ty) => {
try_from_string!($type, &str);
try_from_string!($type, &String);
};
($type:ty, $from_type:ty) => {
impl TryFrom<$from_type> for $type {
type Error = $crate::errors::TryFromStrError;
fn try_from(value: $from_type) -> Result<Self, Self::Error> {
let first_char = value
.chars()
.nth(0)
.ok_or($crate::errors::TryFromStrError)?;
Self::try_from(first_char).map_err(|_| $crate::errors::TryFromStrError)
}
}
};
}
#[macro_export]
macro_rules! piece {
($color:ident $shape:ident) => {

View file

@ -1,124 +1,10 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{errors::TryFromCharError, try_from_string, Color, Square};
use std::{array, fmt, slice};
mod display;
trait _Shape {
fn symbol(&self) -> char;
fn index(&self) -> usize;
}
pub use self::display::{PieceDisplay, PieceDisplayStyle};
macro_rules! shape {
($name:ident, $index:expr, $symbol:expr) => {
struct $name;
impl _Shape for $name {
fn symbol(&self) -> char {
$symbol
}
fn index(&self) -> usize {
$index
}
}
};
}
shape!(Pawn, 0, 'P');
shape!(Knight, 1, 'K');
shape!(Bishop, 2, 'B');
shape!(Rook, 3, 'R');
shape!(Queen, 4, 'Q');
shape!(King, 5, 'K');
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Shape {
Pawn = 0,
Knight = 1,
Bishop = 2,
Rook = 3,
Queen = 4,
King = 5,
}
impl Shape {
/// Number of piece shapes
pub const NUM: usize = 6;
/// A slice of all piece shapes
pub const ALL: [Shape; Self::NUM] = [
Shape::Pawn,
Shape::Knight,
Shape::Bishop,
Shape::Rook,
Shape::Queen,
Shape::King,
];
pub fn iter() -> slice::Iter<'static, Self> {
Shape::ALL.iter()
}
pub fn into_iter() -> array::IntoIter<Self, { Self::NUM }> {
Shape::ALL.into_iter()
}
/// An iterator over the shapes that a pawn can promote to
pub fn promotable() -> slice::Iter<'static, Shape> {
const PROMOTABLE_SHAPES: [Shape; 4] =
[Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
PROMOTABLE_SHAPES.iter()
}
const fn to_ascii(self) -> char {
match self {
Shape::Pawn => 'P',
Shape::Knight => 'N',
Shape::Bishop => 'B',
Shape::Rook => 'R',
Shape::Queen => 'Q',
Shape::King => 'K',
}
}
}
impl TryFrom<char> for Shape {
type Error = TryFromCharError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'P' | 'p' => Ok(Shape::Pawn),
'N' | 'n' => Ok(Shape::Knight),
'B' | 'b' => Ok(Shape::Bishop),
'R' | 'r' => Ok(Shape::Rook),
'Q' | 'q' => Ok(Shape::Queen),
'K' | 'k' => Ok(Shape::King),
_ => Err(TryFromCharError),
}
}
}
try_from_string!(Shape);
impl From<&Shape> for char {
fn from(shape: &Shape) -> char {
char::from(*shape)
}
}
impl From<Shape> for char {
fn from(shape: Shape) -> char {
shape.to_ascii()
}
}
impl fmt::Display for Shape {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let self_char: char = self.into();
write!(f, "{self_char}")
}
}
use crate::{Color, Shape, Square};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Piece {
@ -195,9 +81,16 @@ impl Piece {
}
}
impl fmt::Display for Piece {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_unicode())
impl std::fmt::Display for Piece {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
PieceDisplay {
piece: *self,
style: PieceDisplayStyle::default()
}
)
}
}
@ -268,8 +161,8 @@ impl PlacedPiece {
}
}
impl fmt::Display for PlacedPiece {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for PlacedPiece {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.piece, self.square)
}
}

View file

@ -0,0 +1,28 @@
// Eryn Wells <eryn@erynwells.me>
use super::Piece;
#[derive(Default)]
pub enum PieceDisplayStyle {
#[default]
Unicode,
ASCII,
LongForm,
}
pub struct PieceDisplay {
pub(super) piece: Piece,
pub(super) style: PieceDisplayStyle,
}
impl std::fmt::Display for PieceDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.style {
PieceDisplayStyle::Unicode => write!(f, "{}", self.piece.to_unicode()),
PieceDisplayStyle::ASCII => write!(f, "{}", self.piece.to_ascii()),
PieceDisplayStyle::LongForm => {
write!(f, "{} {}", self.piece.color.name(), self.piece.shape.name())
}
}
}
}

137
core/src/shapes.rs Normal file
View file

@ -0,0 +1,137 @@
// Eryn Wells <eryn@erynwells.me>
use std::{array, slice};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Shape {
Pawn = 0,
Knight = 1,
Bishop = 2,
Rook = 3,
Queen = 4,
King = 5,
}
impl Shape {
/// Number of piece shapes
pub const NUM: usize = 6;
/// A slice of all piece shapes
pub const ALL: [Shape; Self::NUM] = [
Shape::Pawn,
Shape::Knight,
Shape::Bishop,
Shape::Rook,
Shape::Queen,
Shape::King,
];
pub fn iter() -> slice::Iter<'static, Self> {
Shape::ALL.iter()
}
#[must_use]
pub fn into_iter() -> array::IntoIter<Self, { Self::NUM }> {
Shape::ALL.into_iter()
}
/// An iterator over the shapes that a pawn can promote to
pub fn promotable() -> slice::Iter<'static, Shape> {
const PROMOTABLE_SHAPES: [Shape; 4] =
[Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
PROMOTABLE_SHAPES.iter()
}
#[must_use]
pub const fn to_ascii(self) -> char {
match self {
Shape::Pawn => 'P',
Shape::Knight => 'N',
Shape::Bishop => 'B',
Shape::Rook => 'R',
Shape::Queen => 'Q',
Shape::King => 'K',
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Shape::Pawn => "pawn",
Shape::Knight => "knight",
Shape::Bishop => "bishop",
Shape::Rook => "rook",
Shape::Queen => "queen",
Shape::King => "king",
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching piece shape for character '{0:?}'")]
pub struct ShapeFromCharError(char);
impl TryFrom<char> for Shape {
type Error = ShapeFromCharError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'P' | 'p' => Ok(Shape::Pawn),
'N' | 'n' => Ok(Shape::Knight),
'B' | 'b' => Ok(Shape::Bishop),
'R' | 'r' => Ok(Shape::Rook),
'Q' | 'q' => Ok(Shape::Queen),
'K' | 'k' => Ok(Shape::King),
_ => Err(ShapeFromCharError(value)),
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching piece shape for string")]
pub struct ShapeFromStrError;
impl TryFrom<&str> for Shape {
type Error = ShapeFromStrError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"p" | "pawn" => Ok(Shape::Pawn),
"n" | "knight" => Ok(Shape::Knight),
"b" | "bishop" => Ok(Shape::Bishop),
"r" | "rook" => Ok(Shape::Rook),
"q" | "queen" => Ok(Shape::Queen),
"k" | "king" => Ok(Shape::King),
_ => Err(ShapeFromStrError),
}
}
}
impl std::str::FromStr for Shape {
type Err = ShapeFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl From<&Shape> for char {
fn from(shape: &Shape) -> char {
char::from(*shape)
}
}
impl From<Shape> for char {
fn from(shape: Shape) -> char {
shape.to_ascii()
}
}
impl std::fmt::Display for Shape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let self_char: char = self.into();
write!(f, "{self_char}")
}
}