patient-matching 0.2.0

Patient matching algorithms for healthcare information exchange
Documentation
//! Error types for patient-matching operations.
//!
//! The crate uses a single sum-type, [`MatchingError`], for every fallible
//! operation, and a [`Result`] alias to keep call-sites concise.
//!
//! The matching engine itself is **infallible**: scoring two patients always
//! produces a [`crate::MatchResult`]. Errors arise from explicit validation
//! steps such as [`crate::Patient::validate`].
//!
//! ## Example
//!
//! ```
//! use patient_matching::{MatchingError, Patient};
//!
//! let empty = Patient::builder().build();
//! match empty.validate() {
//!     Err(MatchingError::MissingField(msg)) => {
//!         assert!(msg.contains("required"));
//!     }
//!     other => panic!("unexpected: {other:?}"),
//! }
//! ```

use thiserror::Error;

/// Result alias used throughout the crate.
///
/// Equivalent to `std::result::Result<T, MatchingError>`.
///
/// ```
/// use patient_matching::Result;
/// fn doubled(x: i32) -> Result<i32> { Ok(x * 2) }
/// assert_eq!(doubled(3).unwrap(), 6);
/// ```
pub type Result<T> = std::result::Result<T, MatchingError>;

/// Errors that may be returned by patient-matching operations.
///
/// The variants are deliberately broad — most consumers only need to
/// distinguish "the input was unusable" from "the system was misconfigured".
///
/// ```
/// use patient_matching::MatchingError;
///
/// let e = MatchingError::MissingField("nhs_number".into());
/// // `Display` is provided by `thiserror`.
/// assert!(e.to_string().contains("Missing required field"));
/// ```
#[derive(Error, Debug)]
pub enum MatchingError {
    /// Generic "the input data did not make sense" error.
    #[error("Invalid patient data: {0}")]
    InvalidData(String),

    /// A required field was absent. Returned by [`crate::Patient::validate`].
    #[error("Missing required field: {0}")]
    MissingField(String),

    /// An NHS number failed to parse via the `nhs-number` crate.
    #[error("Invalid NHS number: {0}")]
    InvalidNhsNumber(String),

    /// A date string could not be parsed into a calendar date.
    #[error("Invalid date format: {0}")]
    InvalidDate(String),

    /// A [`crate::MatchConfig`] value was out of range or self-inconsistent.
    #[error("Configuration error: {0}")]
    ConfigError(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn missing_field_display() {
        let e = MatchingError::MissingField("nhs_number".into());
        assert_eq!(e.to_string(), "Missing required field: nhs_number");
    }

    #[test]
    fn invalid_data_display() {
        let e = MatchingError::InvalidData("oops".into());
        assert_eq!(e.to_string(), "Invalid patient data: oops");
    }

    #[test]
    fn invalid_nhs_display() {
        let e = MatchingError::InvalidNhsNumber("ABCDEFGHIJ".into());
        assert!(e.to_string().contains("Invalid NHS number"));
    }

    #[test]
    fn invalid_date_display() {
        let e = MatchingError::InvalidDate("2024-13-32".into());
        assert!(e.to_string().contains("Invalid date format"));
    }

    #[test]
    fn config_error_display() {
        let e = MatchingError::ConfigError("threshold > 1.0".into());
        assert!(e.to_string().contains("Configuration error"));
    }

    #[test]
    fn result_alias_resolves() {
        fn make() -> Result<i32> {
            Ok(42)
        }
        assert_eq!(make().unwrap(), 42);
    }

    #[test]
    fn errors_are_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<MatchingError>();
    }
}