1use crate::polygonal::PolygonValidationError;
27#[cfg(feature = "grid")]
28use crate::{FlowFieldBuildError, GridBuildError, GridEditError, GridSearchError};
29#[cfg(feature = "navmesh")]
30use crate::{
31 PreparedNavmeshBuildError,
32 navmesh::{DynamicNavmeshError, NavmeshValidationError},
33};
34
35#[derive(Debug, thiserror::Error)]
41#[non_exhaustive]
42pub enum ArtifactDataError {
43 #[error("I/O failure: {0}")]
45 Io(#[from] std::io::Error),
46 #[error("invalid UTF-8: {0}")]
48 Utf8(#[from] std::string::FromUtf8Error),
49 #[error("invalid integer: {0}")]
51 ParseInt(#[from] std::num::ParseIntError),
52 #[error("invalid floating-point number: {0}")]
54 ParseFloat(#[from] std::num::ParseFloatError),
55 #[error("invalid TOML: {0}")]
57 Toml(#[from] toml::de::Error),
58 #[error("invalid JSON: {0}")]
60 Json(#[from] serde_json::Error),
61 #[cfg(feature = "grid")]
63 #[error("grid construction failed: {0}")]
64 GridBuild(#[from] GridBuildError),
65 #[cfg(feature = "grid")]
67 #[error("grid edit failed: {0}")]
68 GridEdit(#[from] GridEditError),
69 #[cfg(feature = "grid")]
71 #[error("grid search failed: {0}")]
72 GridSearch(#[from] GridSearchError),
73 #[error("polygon validation failed: {0}")]
75 PolygonValidation(#[from] PolygonValidationError),
76 #[cfg(feature = "navmesh")]
78 #[error("navmesh validation failed: {0}")]
79 NavmeshValidation(#[from] NavmeshValidationError),
80 #[cfg(feature = "navmesh")]
82 #[error("dynamic navmesh validation failed: {0}")]
83 DynamicNavmesh(#[from] DynamicNavmeshError),
84 #[cfg(feature = "navmesh")]
86 #[error("prepared navmesh construction failed: {0}")]
87 PreparedNavmesh(#[from] PreparedNavmeshBuildError),
88 #[cfg(feature = "grid")]
90 #[error("flow-field construction failed: {0}")]
91 FlowField(#[from] FlowFieldBuildError),
92 #[error(transparent)]
94 Contract(Box<ArtifactContractError>),
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum ArtifactContractKind {
101 UnsupportedVersion,
103 MissingRequiredField,
105 EmptyIdentifier,
107 EmptyCollection,
109 OutOfBounds,
111 InvalidReference,
113 InconsistentData,
115 InvalidValue,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
121#[non_exhaustive]
122pub enum ArtifactContractError {
123 #[error("unsupported {field} {value} for {artifact}")]
125 UnsupportedVersion {
126 artifact: String,
128 field: String,
130 value: String,
132 },
133 #[error("{artifact} is missing required field {field}")]
135 MissingRequiredField {
136 artifact: String,
138 field: String,
140 },
141 #[error("{artifact} has an empty identifier in {field}")]
143 EmptyIdentifier {
144 artifact: String,
146 field: String,
148 },
149 #[error("{artifact} has an empty collection in {field}")]
151 EmptyCollection {
152 artifact: String,
154 field: String,
156 },
157 #[error("{artifact} field {field} is out of bounds at {location:?}: {value}")]
159 OutOfBounds {
160 artifact: String,
162 field: String,
164 location: ArtifactContractLocation,
166 value: String,
168 },
169 #[error("{artifact} field {field} has an invalid reference at {location:?}: {value}")]
171 InvalidReference {
172 artifact: String,
174 field: String,
176 location: ArtifactContractLocation,
178 value: String,
180 },
181 #[error("{artifact} fields are inconsistent at {location:?}: {field}={value}")]
183 InconsistentData {
184 artifact: String,
186 field: String,
188 location: ArtifactContractLocation,
190 value: String,
192 },
193 #[error("{artifact} field {field} has invalid value at {location:?}: {value}")]
195 InvalidValue {
196 artifact: String,
198 field: String,
200 location: ArtifactContractLocation,
202 value: String,
204 },
205}
206
207#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
213#[non_exhaustive]
214pub struct ArtifactContractLocation {
215 pub index: Option<usize>,
217 pub row: Option<usize>,
219 pub column: Option<usize>,
221}
222
223impl ArtifactContractLocation {
224 pub const NONE: Self = Self {
226 index: None,
227 row: None,
228 column: None,
229 };
230
231 pub const fn index(index: usize) -> Self {
233 Self {
234 index: Some(index),
235 row: None,
236 column: None,
237 }
238 }
239
240 pub const fn row(row: usize) -> Self {
242 Self {
243 index: None,
244 row: Some(row),
245 column: None,
246 }
247 }
248
249 pub const fn row_column(row: usize, column: usize) -> Self {
251 Self {
252 index: None,
253 row: Some(row),
254 column: Some(column),
255 }
256 }
257}
258
259impl ArtifactContractError {
260 pub const fn kind(&self) -> ArtifactContractKind {
265 match self {
266 Self::UnsupportedVersion { .. } => ArtifactContractKind::UnsupportedVersion,
267 Self::MissingRequiredField { .. } => ArtifactContractKind::MissingRequiredField,
268 Self::EmptyIdentifier { .. } => ArtifactContractKind::EmptyIdentifier,
269 Self::EmptyCollection { .. } => ArtifactContractKind::EmptyCollection,
270 Self::OutOfBounds { .. } => ArtifactContractKind::OutOfBounds,
271 Self::InvalidReference { .. } => ArtifactContractKind::InvalidReference,
272 Self::InconsistentData { .. } => ArtifactContractKind::InconsistentData,
273 Self::InvalidValue { .. } => ArtifactContractKind::InvalidValue,
274 }
275 }
276}
277
278impl ArtifactDataError {
279 pub(crate) fn unsupported_version(artifact: impl Into<String>, value: impl ToString) -> Self {
281 Self::Contract(Box::new(ArtifactContractError::UnsupportedVersion {
282 artifact: artifact.into(),
283 field: "format_version".into(),
284 value: value.to_string(),
285 }))
286 }
287
288 pub(crate) fn missing_required_field(
290 artifact: impl Into<String>,
291 field: impl Into<String>,
292 ) -> Self {
293 Self::Contract(Box::new(ArtifactContractError::MissingRequiredField {
294 artifact: artifact.into(),
295 field: field.into(),
296 }))
297 }
298
299 pub(crate) fn empty_identifier(artifact: impl Into<String>, field: impl Into<String>) -> Self {
301 Self::Contract(Box::new(ArtifactContractError::EmptyIdentifier {
302 artifact: artifact.into(),
303 field: field.into(),
304 }))
305 }
306
307 pub(crate) fn invalid_reference(
309 artifact: impl Into<String>,
310 field: impl Into<String>,
311 location: ArtifactContractLocation,
312 value: impl ToString,
313 ) -> Self {
314 Self::Contract(Box::new(ArtifactContractError::InvalidReference {
315 artifact: artifact.into(),
316 field: field.into(),
317 location,
318 value: value.to_string(),
319 }))
320 }
321
322 pub(crate) fn inconsistent_data(
324 artifact: impl Into<String>,
325 field: impl Into<String>,
326 location: ArtifactContractLocation,
327 value: impl ToString,
328 ) -> Self {
329 Self::Contract(Box::new(ArtifactContractError::InconsistentData {
330 artifact: artifact.into(),
331 field: field.into(),
332 location,
333 value: value.to_string(),
334 }))
335 }
336
337 pub(crate) fn invalid_value(
339 artifact: impl Into<String>,
340 field: impl Into<String>,
341 location: ArtifactContractLocation,
342 value: impl ToString,
343 ) -> Self {
344 Self::Contract(Box::new(ArtifactContractError::InvalidValue {
345 artifact: artifact.into(),
346 field: field.into(),
347 location,
348 value: value.to_string(),
349 }))
350 }
351}
352
353#[derive(Debug, thiserror::Error)]
359#[non_exhaustive]
360pub enum ArtifactLoadError {
361 #[error("failed to load Moving AI atlas data: {source}")]
363 Atlas {
364 #[source]
366 source: ArtifactDataError,
367 },
368 #[error("failed to load benchmark scenario data: {source}")]
370 Benchmark {
371 #[source]
373 source: ArtifactDataError,
374 },
375 #[error("failed to load polygon scene data: {source}")]
377 Polygon {
378 #[source]
380 source: ArtifactDataError,
381 },
382 #[error("failed to load navmesh data: {source}")]
384 Navmesh {
385 #[source]
387 source: ArtifactDataError,
388 },
389 #[error("failed to load flow-field data: {source}")]
391 FlowField {
392 #[source]
394 source: ArtifactDataError,
395 },
396 #[error("failed to load MAPF data: {source}")]
398 Mapf {
399 #[source]
401 source: ArtifactDataError,
402 },
403 #[error("failed to load replanning data: {source}")]
405 Replanning {
406 #[source]
408 source: ArtifactDataError,
409 },
410 #[error("failed to load any-angle benchmark data: {source}")]
412 AnyAngle {
413 #[source]
415 source: ArtifactDataError,
416 },
417}
418
419impl ArtifactLoadError {
420 pub(crate) const fn polygon(source: ArtifactDataError) -> Self {
422 Self::Polygon { source }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use std::error::Error;
429
430 use super::{
431 ArtifactContractError, ArtifactContractLocation, ArtifactDataError, ArtifactLoadError,
432 };
433
434 #[test]
435 fn benchmark_toml_error_preserves_concrete_source_chain() {
436 let parse_error = toml::from_str::<u32>("not = [valid").unwrap_err();
437 let error = ArtifactLoadError::Benchmark {
438 source: parse_error.into(),
439 };
440
441 assert!(matches!(
442 &error,
443 ArtifactLoadError::Benchmark {
444 source: ArtifactDataError::Toml(_)
445 }
446 ));
447 let data_source = error
448 .source()
449 .expect("loader error should expose data source");
450 assert!(
451 data_source.source().is_some(),
452 "TOML source should be chained"
453 );
454 }
455
456 #[test]
457 fn unsupported_version_exposes_exact_typed_data() {
458 let error = ArtifactDataError::unsupported_version("benchmark pack", 7);
459 let ArtifactDataError::Contract(contract) = error else {
460 panic!("expected contract error");
461 };
462 assert_eq!(
463 *contract,
464 ArtifactContractError::UnsupportedVersion {
465 artifact: "benchmark pack".into(),
466 field: "format_version".into(),
467 value: "7".into(),
468 }
469 );
470 }
471
472 #[test]
473 fn empty_identifier_exposes_exact_typed_data() {
474 let error = ArtifactDataError::empty_identifier("benchmark pack", "pack_id");
475 let ArtifactDataError::Contract(contract) = error else {
476 panic!("expected contract error");
477 };
478 assert_eq!(
479 *contract,
480 ArtifactContractError::EmptyIdentifier {
481 artifact: "benchmark pack".into(),
482 field: "pack_id".into(),
483 }
484 );
485 }
486
487 #[test]
488 fn missing_field_exposes_exact_typed_data() {
489 let error = ArtifactDataError::missing_required_field("scenario-1", "source_map");
490 let ArtifactDataError::Contract(contract) = error else {
491 panic!("expected contract error");
492 };
493 assert_eq!(
494 *contract,
495 ArtifactContractError::MissingRequiredField {
496 artifact: "scenario-1".into(),
497 field: "source_map".into(),
498 }
499 );
500 }
501
502 #[test]
503 fn invalid_reference_exposes_exact_typed_data() {
504 let error = ArtifactDataError::invalid_reference(
505 "mesh-1",
506 "portals.left",
507 ArtifactContractLocation::index(3),
508 "missing-cell",
509 );
510 let ArtifactDataError::Contract(contract) = error else {
511 panic!("expected contract error");
512 };
513 assert_eq!(
514 *contract,
515 ArtifactContractError::InvalidReference {
516 artifact: "mesh-1".into(),
517 field: "portals.left".into(),
518 location: ArtifactContractLocation::index(3),
519 value: "missing-cell".into(),
520 }
521 );
522 }
523
524 #[test]
525 fn invalid_value_exposes_exact_typed_data() {
526 let error = ArtifactDataError::invalid_value(
527 "scenario-1",
528 "movement_model",
529 ArtifactContractLocation::NONE,
530 "hex",
531 );
532 let ArtifactDataError::Contract(contract) = error else {
533 panic!("expected contract error");
534 };
535 assert_eq!(
536 *contract,
537 ArtifactContractError::InvalidValue {
538 artifact: "scenario-1".into(),
539 field: "movement_model".into(),
540 location: ArtifactContractLocation::NONE,
541 value: "hex".into(),
542 }
543 );
544 }
545}