1use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(tag = "type", content = "data")]
29pub enum Error {
30 #[error("invalid handle: {0}")]
33 InvalidHandle(u32),
34
35 #[error("body not found: handle={0}")]
37 BodyNotFound(u32),
38
39 #[error("collider not found: handle={0}")]
41 ColliderNotFound(u32),
42
43 #[error("invalid parameter '{name}': {message}")]
46 InvalidParameter {
47 name: String,
49 message: String,
51 },
52
53 #[error("mass must be positive, got {0}")]
55 InvalidMass(f64),
56
57 #[error("time step must be positive, got {0}")]
59 InvalidTimeStep(f64),
60
61 #[error("dimension must be positive, got {0}")]
63 InvalidDimension(f64),
64
65 #[error("world at capacity: max {max} bodies")]
68 CapacityExceeded {
69 max: usize,
71 },
72
73 #[error("simulation diverged at step {step}")]
76 SimulationDiverged {
77 step: u64,
79 },
80
81 #[error("solver did not converge after {iterations} iterations")]
83 SolverConvergenceFailed {
84 iterations: u32,
86 },
87
88 #[error("body {0} is sleeping")]
90 BodySleeping(u32),
91
92 #[error("serialization error: {0}")]
95 Serialization(String),
96
97 #[error("snapshot validation failed: {0}")]
99 SnapshotValidation(String),
100
101 #[error("type error: expected {expected}, got {got}")]
104 TypeError {
105 expected: String,
107 got: String,
109 },
110
111 #[error("missing argument: '{0}'")]
113 MissingArgument(String),
114
115 #[error("wrong array length: expected {expected}, got {got}")]
117 WrongArrayLength {
118 expected: usize,
120 got: usize,
122 },
123
124 #[error("{0}")]
127 General(String),
128}
129
130pub type Result<T> = std::result::Result<T, Error>;
132
133impl Error {
138 pub fn invalid_param(name: impl Into<String>, message: impl Into<String>) -> Self {
142 Self::InvalidParameter {
143 name: name.into(),
144 message: message.into(),
145 }
146 }
147
148 pub fn general(msg: impl Into<String>) -> Self {
150 Self::General(msg.into())
151 }
152
153 pub fn type_error(expected: impl Into<String>, got: impl Into<String>) -> Self {
155 Self::TypeError {
156 expected: expected.into(),
157 got: got.into(),
158 }
159 }
160
161 pub fn wrong_len(expected: usize, got: usize) -> Self {
163 Self::WrongArrayLength { expected, got }
164 }
165
166 pub fn is_handle_error(&self) -> bool {
170 matches!(
171 self,
172 Error::InvalidHandle(_) | Error::BodyNotFound(_) | Error::ColliderNotFound(_)
173 )
174 }
175
176 pub fn is_parameter_error(&self) -> bool {
178 matches!(
179 self,
180 Error::InvalidParameter { .. }
181 | Error::InvalidMass(_)
182 | Error::InvalidTimeStep(_)
183 | Error::InvalidDimension(_)
184 )
185 }
186
187 pub fn is_capacity_error(&self) -> bool {
189 matches!(self, Error::CapacityExceeded { .. })
190 }
191
192 pub fn is_stability_error(&self) -> bool {
194 matches!(
195 self,
196 Error::SimulationDiverged { .. } | Error::SolverConvergenceFailed { .. }
197 )
198 }
199
200 pub fn is_serialization_error(&self) -> bool {
202 matches!(self, Error::Serialization(_) | Error::SnapshotValidation(_))
203 }
204
205 pub fn is_type_error(&self) -> bool {
207 matches!(
208 self,
209 Error::TypeError { .. } | Error::MissingArgument(_) | Error::WrongArrayLength { .. }
210 )
211 }
212
213 pub fn to_json(&self) -> String {
227 let variant_json =
228 serde_json::to_string(self).unwrap_or_else(|_| "\"<serialization failed>\"".into());
229 let message = self.to_string();
230 format!(
231 r#"{{"error":{variant_json},"message":{message_json}}}"#,
232 variant_json = variant_json,
233 message_json = serde_json::to_string(&message).unwrap_or_default(),
234 )
235 }
236
237 pub fn from_json(json: &str) -> std::result::Result<Self, String> {
239 let direct: std::result::Result<Error, _> = serde_json::from_str(json);
241 if let Ok(e) = direct {
242 return Ok(e);
243 }
244 let v: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
246 let inner = v
247 .get("error")
248 .ok_or_else(|| "missing 'error' field".to_string())?;
249 serde_json::from_value(inner.clone()).map_err(|e| e.to_string())
250 }
251
252 pub fn python_exception_class(&self) -> &'static str {
256 match self {
257 Error::TypeError { .. } | Error::WrongArrayLength { .. } => "TypeError",
258 Error::MissingArgument(_) => "ValueError",
259 Error::InvalidParameter { .. }
260 | Error::InvalidMass(_)
261 | Error::InvalidTimeStep(_)
262 | Error::InvalidDimension(_) => "ValueError",
263 Error::InvalidHandle(_) | Error::BodyNotFound(_) | Error::ColliderNotFound(_) => {
264 "KeyError"
265 }
266 Error::CapacityExceeded { .. } => "MemoryError",
267 Error::SimulationDiverged { .. } | Error::SolverConvergenceFailed { .. } => {
268 "RuntimeError"
269 }
270 Error::Serialization(_) | Error::SnapshotValidation(_) => "ValueError",
271 _ => "RuntimeError",
272 }
273 }
274
275 pub fn recovery_hint(&self) -> String {
277 match self {
278 Error::InvalidTimeStep(_) => {
279 "Use a positive dt such as 1/60 for a 60 Hz simulation.".into()
280 }
281 Error::InvalidMass(_) => "Mass must be strictly positive (e.g. 1.0).".into(),
282 Error::BodyNotFound(h) => format!(
283 "Body handle {} not found. Re-add the body or check it has not been removed.",
284 h
285 ),
286 Error::CapacityExceeded { max } => format!(
287 "World capacity of {} bodies reached. Remove unused bodies first.",
288 max
289 ),
290 Error::SimulationDiverged { step } => format!(
291 "Divergence detected at step {}. Reduce dt or applied forces.",
292 step
293 ),
294 Error::TypeError { expected, got } => {
295 format!("Expected a Python '{}' but received '{}'.", expected, got)
296 }
297 Error::WrongArrayLength { expected, got } => {
298 format!("Array length should be {} but is {}.", expected, got)
299 }
300 _ => String::new(),
301 }
302 }
303}
304
305#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
316 fn test_invalid_handle() {
317 let e = Error::InvalidHandle(5);
318 assert!(e.to_string().contains("5"));
319 assert!(e.is_handle_error());
320 }
321
322 #[test]
323 fn test_body_not_found() {
324 let e = Error::BodyNotFound(10);
325 assert!(e.to_string().contains("10"));
326 assert!(e.is_handle_error());
327 }
328
329 #[test]
330 fn test_collider_not_found() {
331 let e = Error::ColliderNotFound(3);
332 assert!(e.is_handle_error());
333 }
334
335 #[test]
336 fn test_invalid_param() {
337 let e = Error::invalid_param("mass", "must be positive");
338 assert!(e.to_string().contains("mass"));
339 assert!(e.to_string().contains("positive"));
340 assert!(e.is_parameter_error());
341 }
342
343 #[test]
344 fn test_invalid_mass() {
345 let e = Error::InvalidMass(-1.0);
346 assert!(e.is_parameter_error());
347 assert!(e.to_string().contains("-1"));
348 }
349
350 #[test]
351 fn test_invalid_time_step() {
352 let e = Error::InvalidTimeStep(0.0);
353 assert!(e.is_parameter_error());
354 }
355
356 #[test]
357 fn test_invalid_dimension() {
358 let e = Error::InvalidDimension(-0.5);
359 assert!(e.is_parameter_error());
360 }
361
362 #[test]
363 fn test_capacity_exceeded() {
364 let e = Error::CapacityExceeded { max: 1000 };
365 assert!(e.is_capacity_error());
366 assert!(e.to_string().contains("1000"));
367 }
368
369 #[test]
370 fn test_simulation_diverged() {
371 let e = Error::SimulationDiverged { step: 99 };
372 assert!(e.is_stability_error());
373 assert!(e.to_string().contains("99"));
374 }
375
376 #[test]
377 fn test_solver_convergence_failed() {
378 let e = Error::SolverConvergenceFailed { iterations: 50 };
379 assert!(e.is_stability_error());
380 assert!(e.to_string().contains("50"));
381 }
382
383 #[test]
384 fn test_body_sleeping() {
385 let e = Error::BodySleeping(7);
386 assert!(e.to_string().contains("7"));
387 }
388
389 #[test]
390 fn test_serialization_error() {
391 let e = Error::Serialization("unexpected eof".into());
392 assert!(e.is_serialization_error());
393 }
394
395 #[test]
396 fn test_snapshot_validation_error() {
397 let e = Error::SnapshotValidation("missing version".into());
398 assert!(e.is_serialization_error());
399 }
400
401 #[test]
402 fn test_type_error() {
403 let e = Error::type_error("list", "int");
404 assert!(e.is_type_error());
405 assert!(e.to_string().contains("list"));
406 assert!(e.to_string().contains("int"));
407 }
408
409 #[test]
410 fn test_missing_argument() {
411 let e = Error::MissingArgument("mass".into());
412 assert!(e.is_type_error());
413 assert!(e.to_string().contains("mass"));
414 }
415
416 #[test]
417 fn test_wrong_array_length() {
418 let e = Error::wrong_len(3, 2);
419 assert!(e.is_type_error());
420 assert!(e.to_string().contains("3"));
421 assert!(e.to_string().contains("2"));
422 }
423
424 #[test]
425 fn test_general_error() {
426 let e = Error::general("oops");
427 assert!(e.to_string().contains("oops"));
428 }
429
430 #[test]
433 fn test_clone_eq() {
434 let e1 = Error::InvalidHandle(42);
435 let e2 = e1.clone();
436 assert_eq!(e1, e2);
437 }
438
439 #[test]
442 fn test_to_json_contains_type() {
443 let e = Error::InvalidTimeStep(-0.01);
444 let json = e.to_json();
445 assert!(json.contains("InvalidTimeStep"), "json={}", json);
446 assert!(json.contains("message"), "json={}", json);
447 }
448
449 #[test]
450 fn test_from_json_direct() {
451 let original = Error::BodyNotFound(7);
452 let json = serde_json::to_string(&original).unwrap();
453 let recovered = Error::from_json(&json).unwrap();
454 assert_eq!(original, recovered);
455 }
456
457 #[test]
458 fn test_from_json_envelope() {
459 let e = Error::CapacityExceeded { max: 256 };
460 let envelope = e.to_json();
461 let recovered = Error::from_json(&envelope).unwrap();
462 assert_eq!(recovered, e);
463 }
464
465 #[test]
466 fn test_from_json_invalid() {
467 assert!(Error::from_json("{bad json").is_err());
468 }
469
470 #[test]
473 fn test_python_exception_class_value_error() {
474 assert_eq!(
475 Error::InvalidTimeStep(0.0).python_exception_class(),
476 "ValueError"
477 );
478 assert_eq!(
479 Error::InvalidMass(-1.0).python_exception_class(),
480 "ValueError"
481 );
482 assert_eq!(
483 Error::invalid_param("x", "y").python_exception_class(),
484 "ValueError"
485 );
486 }
487
488 #[test]
489 fn test_python_exception_class_key_error() {
490 assert_eq!(Error::BodyNotFound(1).python_exception_class(), "KeyError");
491 assert_eq!(Error::InvalidHandle(0).python_exception_class(), "KeyError");
492 }
493
494 #[test]
495 fn test_python_exception_class_type_error() {
496 assert_eq!(
497 Error::type_error("list", "int").python_exception_class(),
498 "TypeError"
499 );
500 assert_eq!(Error::wrong_len(3, 1).python_exception_class(), "TypeError");
501 }
502
503 #[test]
504 fn test_python_exception_class_runtime_error() {
505 assert_eq!(
506 Error::SimulationDiverged { step: 1 }.python_exception_class(),
507 "RuntimeError"
508 );
509 assert_eq!(
510 Error::SolverConvergenceFailed { iterations: 10 }.python_exception_class(),
511 "RuntimeError"
512 );
513 }
514
515 #[test]
518 fn test_recovery_hint_time_step() {
519 let hint = Error::InvalidTimeStep(0.0).recovery_hint();
520 assert!(!hint.is_empty());
521 }
522
523 #[test]
524 fn test_recovery_hint_body_not_found() {
525 let hint = Error::BodyNotFound(42).recovery_hint();
526 assert!(hint.contains("42"));
527 }
528
529 #[test]
530 fn test_recovery_hint_capacity() {
531 let hint = Error::CapacityExceeded { max: 500 }.recovery_hint();
532 assert!(hint.contains("500"));
533 }
534
535 #[test]
536 fn test_recovery_hint_diverged() {
537 let hint = Error::SimulationDiverged { step: 7 }.recovery_hint();
538 assert!(hint.contains("7"));
539 }
540
541 #[test]
542 fn test_recovery_hint_type_error() {
543 let hint = Error::type_error("ndarray", "str").recovery_hint();
544 assert!(hint.contains("ndarray"));
545 }
546
547 #[test]
548 fn test_recovery_hint_wrong_len() {
549 let hint = Error::wrong_len(3, 1).recovery_hint();
550 assert!(hint.contains("3"));
551 assert!(hint.contains("1"));
552 }
553
554 #[test]
555 fn test_recovery_hint_general_empty() {
556 let hint = Error::general("x").recovery_hint();
558 let _ = hint;
560 }
561}