use std::fmt;
use std::path::PathBuf;
use thiserror::Error;
use serde::{Serialize, Deserialize};
#[cfg(feature = "arrow-export")]
use arrow;
#[cfg(feature = "arrow-export")]
use parquet;
#[cfg(feature = "arrow-export")]
use arrow::error::ArrowError;
#[cfg(feature = "arrow-export")]
use parquet::errors::ParquetError;
pub type Result<T> = std::result::Result<T, NppesError>;
#[derive(Error, Debug)]
pub enum NppesError {
#[error("I/O error: {message}")]
Io {
message: String,
#[source]
source: std::io::Error,
context: ErrorContext,
},
#[error("CSV parsing error at line {line:?}: {message}")]
CsvParse {
message: String,
line: Option<usize>,
column: Option<String>,
context: ErrorContext,
},
#[error("Data validation error: {message}")]
DataValidation {
message: String,
field: Option<String>,
value: Option<String>,
context: ErrorContext,
},
#[error("File not found: {path}")]
FileNotFound {
path: PathBuf,
suggestion: String,
},
#[error("Invalid NPI '{npi}': {reason}")]
InvalidNpi {
npi: String,
reason: String,
suggestion: String,
},
#[error("Invalid entity type code '{code}'")]
InvalidEntityType {
code: String,
valid_options: Vec<String>,
},
#[error("Schema mismatch: {message}")]
SchemaMismatch {
message: String,
expected_columns: Option<usize>,
found_columns: Option<usize>,
mismatched_column: Option<(usize, String, String)>,
},
#[error("Date parsing error: {message}")]
DateParse {
message: String,
value: String,
expected_format: String,
},
#[error("Configuration error: {message}")]
Configuration {
message: String,
suggestion: Option<String>,
},
#[error("Export error: {message}")]
Export {
message: String,
format: ExportFormat,
suggestion: Option<String>,
},
#[error("Memory error: {message}")]
Memory {
message: String,
required_bytes: Option<usize>,
available_bytes: Option<usize>,
},
#[error("Feature '{feature}' is not enabled")]
FeatureNotEnabled {
feature: String,
enable_instruction: String,
},
#[error("{message}")]
Custom {
message: String,
suggestion: Option<String>,
},
}
#[derive(Debug, Default, Clone)]
pub struct ErrorContext {
pub file_path: Option<PathBuf>,
pub line_number: Option<usize>,
pub column_name: Option<String>,
pub record_npi: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub enum ExportFormat {
#[default]
Json,
Csv,
Parquet,
Arrow,
Sql,
}
impl fmt::Display for ExportFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExportFormat::Json => write!(f, "JSON"),
ExportFormat::Csv => write!(f, "CSV"),
ExportFormat::Parquet => write!(f, "Parquet"),
ExportFormat::Arrow => write!(f, "Arrow"),
ExportFormat::Sql => write!(f, "SQL"),
}
}
}
impl NppesError {
pub fn file_not_found_with_suggestion(path: PathBuf) -> Self {
let suggestion = if path.to_string_lossy().contains("npidata") {
format!(
"Check if the file exists at '{}'. Common NPPES files follow the pattern 'npidata_pfile_YYYYMMDD-YYYYMMDD.csv'. \
You can download the latest file from https://download.cms.gov/nppes/NPI_Files.html",
path.display()
)
} else if path.to_string_lossy().contains("taxonomy") {
format!(
"Check if the taxonomy file exists at '{}'. The NUCC taxonomy file can be downloaded from \
https://www.nucc.org/index.php/code-sets-mainmenu-41/provider-taxonomy-mainmenu-40",
path.display()
)
} else {
format!(
"Check if the file exists at '{}'. Make sure the path is correct and you have read permissions.",
path.display()
)
};
Self::FileNotFound { path, suggestion }
}
pub fn invalid_npi(npi: &str) -> Self {
let (reason, suggestion) = if npi.is_empty() {
("NPI cannot be empty".to_string(),
"Provide a valid 10-digit NPI number".to_string())
} else if npi.len() != 10 {
(format!("NPI must be exactly 10 digits, found {}", npi.len()),
"Ensure the NPI is exactly 10 digits without spaces or special characters".to_string())
} else if !npi.chars().all(|c| c.is_ascii_digit()) {
("NPI must contain only digits".to_string(),
"Remove any non-numeric characters from the NPI".to_string())
} else {
("Invalid NPI format".to_string(),
"Verify the NPI number is correct".to_string())
};
Self::InvalidNpi {
npi: npi.to_string(),
reason,
suggestion,
}
}
pub fn invalid_entity_type(code: &str) -> Self {
Self::InvalidEntityType {
code: code.to_string(),
valid_options: vec![
"1 (Individual)".to_string(),
"2 (Organization)".to_string(),
],
}
}
pub fn schema_mismatch_detailed(
expected_columns: usize,
found_columns: usize,
mismatched_column: Option<(usize, String, String)>,
) -> Self {
let message = if let Some((index, expected, found)) = &mismatched_column {
format!(
"Column {} mismatch: expected '{}', found '{}'",
index, expected, found
)
} else {
format!(
"Expected {} columns, found {}",
expected_columns, found_columns
)
};
Self::SchemaMismatch {
message,
expected_columns: Some(expected_columns),
found_columns: Some(found_columns),
mismatched_column,
}
}
pub fn date_parse_with_format(value: &str, expected_format: &str) -> Self {
Self::DateParse {
message: format!("Cannot parse '{}' as date", value),
value: value.to_string(),
expected_format: expected_format.to_string(),
}
}
pub fn insufficient_memory(required: usize, available: Option<usize>) -> Self {
let message = if let Some(avail) = available {
format!(
"Insufficient memory: need {} bytes but only {} bytes available",
format_bytes(required),
format_bytes(avail)
)
} else {
format!("Insufficient memory: need {} bytes", format_bytes(required))
};
Self::Memory {
message,
required_bytes: Some(required),
available_bytes: available,
}
}
pub fn feature_required(feature: &str) -> Self {
let enable_instruction = match feature {
"dataframe" => "Add 'nppes = { version = \"0.2\", features = [\"dataframe\"] }' to your Cargo.toml",
"arrow-export" => "Add 'nppes = { version = \"0.2\", features = [\"arrow-export\"] }' to your Cargo.toml",
"full-text-search" => "Add 'nppes = { version = \"0.2\", features = [\"full-text-search\"] }' to your Cargo.toml",
_ => "Enable the required feature in your Cargo.toml",
};
Self::FeatureNotEnabled {
feature: feature.to_string(),
enable_instruction: enable_instruction.to_string(),
}
}
pub fn user_message(&self) -> String {
match self {
Self::FileNotFound { suggestion, .. } => {
format!("{}\n\nSuggestion: {}", self, suggestion)
}
Self::InvalidNpi { suggestion, .. } => {
format!("{}\n\nSuggestion: {}", self, suggestion)
}
Self::InvalidEntityType { valid_options, .. } => {
format!("{}\n\nValid options: {}", self, valid_options.join(", "))
}
Self::DateParse { expected_format, .. } => {
format!("{}\n\nExpected format: {}", self, expected_format)
}
Self::FeatureNotEnabled { enable_instruction, .. } => {
format!("{}\n\nTo enable: {}", self, enable_instruction)
}
Self::Custom { suggestion: Some(sug), .. } => {
format!("{}\n\nSuggestion: {}", self, sug)
}
_ => self.to_string(),
}
}
}
fn format_bytes(bytes: usize) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.2} {}", size, UNITS[unit_index])
}
impl From<std::io::Error> for NppesError {
fn from(err: std::io::Error) -> Self {
Self::Io {
message: err.to_string(),
source: err,
context: ErrorContext::default(),
}
}
}
impl From<csv::Error> for NppesError {
fn from(err: csv::Error) -> Self {
let (line, message) = match err.position() {
Some(pos) => (Some(pos.line() as usize), err.to_string()),
None => (None, err.to_string()),
};
Self::CsvParse {
message,
line,
column: None,
context: ErrorContext::default(),
}
}
}
impl From<serde_json::Error> for NppesError {
fn from(err: serde_json::Error) -> Self {
NppesError::Export {
message: err.to_string(),
format: ExportFormat::Json,
suggestion: Some("Check if the data is serializable to JSON.".to_string()),
}
}
}
#[cfg(feature = "arrow-export")]
impl From<ArrowError> for NppesError {
fn from(err: ArrowError) -> Self {
NppesError::Export {
message: err.to_string(),
format: ExportFormat::Arrow,
suggestion: Some("Check Arrow schema and data consistency.".to_string()),
}
}
}
#[cfg(feature = "arrow-export")]
impl From<ParquetError> for NppesError {
fn from(err: ParquetError) -> Self {
NppesError::Export {
message: err.to_string(),
format: ExportFormat::Parquet,
suggestion: Some("Check Parquet file and schema consistency.".to_string()),
}
}
}