use crate::polygonal::PolygonValidationError;
#[cfg(feature = "grid")]
use crate::{FlowFieldBuildError, GridBuildError, GridEditError, GridSearchError};
#[cfg(feature = "navmesh")]
use crate::{
PreparedNavmeshBuildError,
navmesh::{DynamicNavmeshError, NavmeshValidationError},
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactDataError {
#[error("I/O failure: {0}")]
Io(#[from] std::io::Error),
#[error("invalid UTF-8: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("invalid integer: {0}")]
ParseInt(#[from] std::num::ParseIntError),
#[error("invalid floating-point number: {0}")]
ParseFloat(#[from] std::num::ParseFloatError),
#[error("invalid TOML: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[cfg(feature = "grid")]
#[error("grid construction failed: {0}")]
GridBuild(#[from] GridBuildError),
#[cfg(feature = "grid")]
#[error("grid edit failed: {0}")]
GridEdit(#[from] GridEditError),
#[cfg(feature = "grid")]
#[error("grid search failed: {0}")]
GridSearch(#[from] GridSearchError),
#[error("polygon validation failed: {0}")]
PolygonValidation(#[from] PolygonValidationError),
#[cfg(feature = "navmesh")]
#[error("navmesh validation failed: {0}")]
NavmeshValidation(#[from] NavmeshValidationError),
#[cfg(feature = "navmesh")]
#[error("dynamic navmesh validation failed: {0}")]
DynamicNavmesh(#[from] DynamicNavmeshError),
#[cfg(feature = "navmesh")]
#[error("prepared navmesh construction failed: {0}")]
PreparedNavmesh(#[from] PreparedNavmeshBuildError),
#[cfg(feature = "grid")]
#[error("flow-field construction failed: {0}")]
FlowField(#[from] FlowFieldBuildError),
#[error(transparent)]
Contract(Box<ArtifactContractError>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArtifactContractKind {
UnsupportedVersion,
MissingRequiredField,
EmptyIdentifier,
EmptyCollection,
OutOfBounds,
InvalidReference,
InconsistentData,
InvalidValue,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactContractError {
#[error("unsupported {field} {value} for {artifact}")]
UnsupportedVersion {
artifact: String,
field: String,
value: String,
},
#[error("{artifact} is missing required field {field}")]
MissingRequiredField {
artifact: String,
field: String,
},
#[error("{artifact} has an empty identifier in {field}")]
EmptyIdentifier {
artifact: String,
field: String,
},
#[error("{artifact} has an empty collection in {field}")]
EmptyCollection {
artifact: String,
field: String,
},
#[error("{artifact} field {field} is out of bounds at {location:?}: {value}")]
OutOfBounds {
artifact: String,
field: String,
location: ArtifactContractLocation,
value: String,
},
#[error("{artifact} field {field} has an invalid reference at {location:?}: {value}")]
InvalidReference {
artifact: String,
field: String,
location: ArtifactContractLocation,
value: String,
},
#[error("{artifact} fields are inconsistent at {location:?}: {field}={value}")]
InconsistentData {
artifact: String,
field: String,
location: ArtifactContractLocation,
value: String,
},
#[error("{artifact} field {field} has invalid value at {location:?}: {value}")]
InvalidValue {
artifact: String,
field: String,
location: ArtifactContractLocation,
value: String,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ArtifactContractLocation {
pub index: Option<usize>,
pub row: Option<usize>,
pub column: Option<usize>,
}
impl ArtifactContractLocation {
pub const NONE: Self = Self {
index: None,
row: None,
column: None,
};
pub const fn index(index: usize) -> Self {
Self {
index: Some(index),
row: None,
column: None,
}
}
pub const fn row(row: usize) -> Self {
Self {
index: None,
row: Some(row),
column: None,
}
}
pub const fn row_column(row: usize, column: usize) -> Self {
Self {
index: None,
row: Some(row),
column: Some(column),
}
}
}
impl ArtifactContractError {
pub const fn kind(&self) -> ArtifactContractKind {
match self {
Self::UnsupportedVersion { .. } => ArtifactContractKind::UnsupportedVersion,
Self::MissingRequiredField { .. } => ArtifactContractKind::MissingRequiredField,
Self::EmptyIdentifier { .. } => ArtifactContractKind::EmptyIdentifier,
Self::EmptyCollection { .. } => ArtifactContractKind::EmptyCollection,
Self::OutOfBounds { .. } => ArtifactContractKind::OutOfBounds,
Self::InvalidReference { .. } => ArtifactContractKind::InvalidReference,
Self::InconsistentData { .. } => ArtifactContractKind::InconsistentData,
Self::InvalidValue { .. } => ArtifactContractKind::InvalidValue,
}
}
}
impl ArtifactDataError {
pub(crate) fn unsupported_version(artifact: impl Into<String>, value: impl ToString) -> Self {
Self::Contract(Box::new(ArtifactContractError::UnsupportedVersion {
artifact: artifact.into(),
field: "format_version".into(),
value: value.to_string(),
}))
}
pub(crate) fn missing_required_field(
artifact: impl Into<String>,
field: impl Into<String>,
) -> Self {
Self::Contract(Box::new(ArtifactContractError::MissingRequiredField {
artifact: artifact.into(),
field: field.into(),
}))
}
pub(crate) fn empty_identifier(artifact: impl Into<String>, field: impl Into<String>) -> Self {
Self::Contract(Box::new(ArtifactContractError::EmptyIdentifier {
artifact: artifact.into(),
field: field.into(),
}))
}
pub(crate) fn invalid_reference(
artifact: impl Into<String>,
field: impl Into<String>,
location: ArtifactContractLocation,
value: impl ToString,
) -> Self {
Self::Contract(Box::new(ArtifactContractError::InvalidReference {
artifact: artifact.into(),
field: field.into(),
location,
value: value.to_string(),
}))
}
pub(crate) fn inconsistent_data(
artifact: impl Into<String>,
field: impl Into<String>,
location: ArtifactContractLocation,
value: impl ToString,
) -> Self {
Self::Contract(Box::new(ArtifactContractError::InconsistentData {
artifact: artifact.into(),
field: field.into(),
location,
value: value.to_string(),
}))
}
pub(crate) fn invalid_value(
artifact: impl Into<String>,
field: impl Into<String>,
location: ArtifactContractLocation,
value: impl ToString,
) -> Self {
Self::Contract(Box::new(ArtifactContractError::InvalidValue {
artifact: artifact.into(),
field: field.into(),
location,
value: value.to_string(),
}))
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactLoadError {
#[error("failed to load Moving AI atlas data: {source}")]
Atlas {
#[source]
source: ArtifactDataError,
},
#[error("failed to load benchmark scenario data: {source}")]
Benchmark {
#[source]
source: ArtifactDataError,
},
#[error("failed to load polygon scene data: {source}")]
Polygon {
#[source]
source: ArtifactDataError,
},
#[error("failed to load navmesh data: {source}")]
Navmesh {
#[source]
source: ArtifactDataError,
},
#[error("failed to load flow-field data: {source}")]
FlowField {
#[source]
source: ArtifactDataError,
},
#[error("failed to load MAPF data: {source}")]
Mapf {
#[source]
source: ArtifactDataError,
},
#[error("failed to load replanning data: {source}")]
Replanning {
#[source]
source: ArtifactDataError,
},
#[error("failed to load any-angle benchmark data: {source}")]
AnyAngle {
#[source]
source: ArtifactDataError,
},
}
impl ArtifactLoadError {
pub(crate) const fn polygon(source: ArtifactDataError) -> Self {
Self::Polygon { source }
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::{
ArtifactContractError, ArtifactContractLocation, ArtifactDataError, ArtifactLoadError,
};
#[test]
fn benchmark_toml_error_preserves_concrete_source_chain() {
let parse_error = toml::from_str::<u32>("not = [valid").unwrap_err();
let error = ArtifactLoadError::Benchmark {
source: parse_error.into(),
};
assert!(matches!(
&error,
ArtifactLoadError::Benchmark {
source: ArtifactDataError::Toml(_)
}
));
let data_source = error
.source()
.expect("loader error should expose data source");
assert!(
data_source.source().is_some(),
"TOML source should be chained"
);
}
#[test]
fn unsupported_version_exposes_exact_typed_data() {
let error = ArtifactDataError::unsupported_version("benchmark pack", 7);
let ArtifactDataError::Contract(contract) = error else {
panic!("expected contract error");
};
assert_eq!(
*contract,
ArtifactContractError::UnsupportedVersion {
artifact: "benchmark pack".into(),
field: "format_version".into(),
value: "7".into(),
}
);
}
#[test]
fn empty_identifier_exposes_exact_typed_data() {
let error = ArtifactDataError::empty_identifier("benchmark pack", "pack_id");
let ArtifactDataError::Contract(contract) = error else {
panic!("expected contract error");
};
assert_eq!(
*contract,
ArtifactContractError::EmptyIdentifier {
artifact: "benchmark pack".into(),
field: "pack_id".into(),
}
);
}
#[test]
fn missing_field_exposes_exact_typed_data() {
let error = ArtifactDataError::missing_required_field("scenario-1", "source_map");
let ArtifactDataError::Contract(contract) = error else {
panic!("expected contract error");
};
assert_eq!(
*contract,
ArtifactContractError::MissingRequiredField {
artifact: "scenario-1".into(),
field: "source_map".into(),
}
);
}
#[test]
fn invalid_reference_exposes_exact_typed_data() {
let error = ArtifactDataError::invalid_reference(
"mesh-1",
"portals.left",
ArtifactContractLocation::index(3),
"missing-cell",
);
let ArtifactDataError::Contract(contract) = error else {
panic!("expected contract error");
};
assert_eq!(
*contract,
ArtifactContractError::InvalidReference {
artifact: "mesh-1".into(),
field: "portals.left".into(),
location: ArtifactContractLocation::index(3),
value: "missing-cell".into(),
}
);
}
#[test]
fn invalid_value_exposes_exact_typed_data() {
let error = ArtifactDataError::invalid_value(
"scenario-1",
"movement_model",
ArtifactContractLocation::NONE,
"hex",
);
let ArtifactDataError::Contract(contract) = error else {
panic!("expected contract error");
};
assert_eq!(
*contract,
ArtifactContractError::InvalidValue {
artifact: "scenario-1".into(),
field: "movement_model".into(),
location: ArtifactContractLocation::NONE,
value: "hex".into(),
}
);
}
}