use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Issue {
DatasetNameTooLong {
dataset: String,
max: usize,
actual: usize,
},
DatasetLabelTooLong {
dataset: String,
max: usize,
actual: usize,
},
VariableNameTooLong {
variable: String,
max: usize,
actual: usize,
},
VariableLabelTooLong {
variable: String,
max: usize,
actual: usize,
},
NumericWrongLength {
variable: String,
expected: usize,
actual: usize,
},
CharacterLengthTooShort {
variable: String,
min: usize,
actual: usize,
},
CharacterLengthTooLong {
variable: String,
max: usize,
actual: usize,
},
RowLenInconsistent {
recorded: usize,
computed: usize,
},
DatasetNamePatternMismatch {
dataset: String,
agency: &'static str,
pattern: String,
},
VariableNamePatternMismatch {
variable: String,
agency: &'static str,
pattern: String,
},
DatasetNameFileStemMismatch {
dataset: String,
stem: String,
},
NonAsciiDatasetName {
dataset: String,
},
NonAsciiVariableName {
variable: String,
},
NonAsciiDatasetLabel {
dataset: String,
},
NonAsciiVariableLabel {
variable: String,
},
AgencyDatasetNameTooLong {
dataset: String,
max: usize,
actual: usize,
},
AgencyVariableNameTooLong {
variable: String,
max: usize,
actual: usize,
},
AgencyLabelTooLong {
name: String,
is_dataset: bool,
max: usize,
actual: usize,
},
CharacterValueLengthExceeded {
variable: String,
length: usize,
agency: &'static str,
max: usize,
},
MultiByteLabelNearLimit {
name: String,
is_dataset: bool,
byte_count: usize,
max_bytes: usize,
char_count: usize,
},
MissingVariableLabel {
variable: String,
},
MissingDatasetLabel {
dataset: String,
},
InvalidFormatSyntax {
variable: String,
format: String,
reason: String,
},
}
impl Issue {
#[must_use]
pub const fn severity(&self) -> Severity {
match self {
Self::CharacterValueLengthExceeded { .. }
| Self::MultiByteLabelNearLimit { .. }
| Self::MissingVariableLabel { .. }
| Self::MissingDatasetLabel { .. } => Severity::Warning,
_ => Severity::Error,
}
}
#[must_use]
pub(crate) fn target(&self) -> Option<Target> {
match self {
Self::DatasetNameTooLong { dataset, .. }
| Self::DatasetLabelTooLong { dataset, .. }
| Self::DatasetNamePatternMismatch { dataset, .. }
| Self::DatasetNameFileStemMismatch { dataset, .. }
| Self::NonAsciiDatasetName { dataset }
| Self::NonAsciiDatasetLabel { dataset }
| Self::AgencyDatasetNameTooLong { dataset, .. }
| Self::MissingDatasetLabel { dataset } => Some(Target::Dataset(dataset.clone())),
Self::VariableNameTooLong { variable, .. }
| Self::VariableLabelTooLong { variable, .. }
| Self::NumericWrongLength { variable, .. }
| Self::CharacterLengthTooShort { variable, .. }
| Self::CharacterLengthTooLong { variable, .. }
| Self::VariableNamePatternMismatch { variable, .. }
| Self::NonAsciiVariableName { variable }
| Self::NonAsciiVariableLabel { variable }
| Self::AgencyVariableNameTooLong { variable, .. }
| Self::CharacterValueLengthExceeded { variable, .. }
| Self::MissingVariableLabel { variable }
| Self::InvalidFormatSyntax { variable, .. } => {
Some(Target::Variable(variable.clone()))
}
Self::AgencyLabelTooLong {
name, is_dataset, ..
}
| Self::MultiByteLabelNearLimit {
name, is_dataset, ..
} => {
if *is_dataset {
Some(Target::Dataset(name.clone()))
} else {
Some(Target::Variable(name.clone()))
}
}
Self::RowLenInconsistent { .. } => None,
}
}
#[must_use]
pub const fn is_error(&self) -> bool {
matches!(self.severity(), Severity::Error)
}
#[must_use]
pub const fn is_warning(&self) -> bool {
matches!(self.severity(), Severity::Warning)
}
}
impl fmt::Display for Issue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] ", self.severity())?;
match self {
Self::DatasetNameTooLong {
dataset,
max,
actual,
}
| Self::AgencyDatasetNameTooLong {
dataset,
max,
actual,
} => {
write!(
f,
"dataset name '{}' exceeds {} bytes (has {} bytes)",
dataset, max, actual
)?;
}
Self::VariableNameTooLong {
variable,
max,
actual,
}
| Self::AgencyVariableNameTooLong {
variable,
max,
actual,
} => {
write!(
f,
"variable name '{}' exceeds {} bytes (has {} bytes)",
variable, max, actual
)?;
}
Self::DatasetLabelTooLong { max, actual, .. } => {
write!(
f,
"dataset label exceeds {} bytes (has {} bytes)",
max, actual
)?;
}
Self::VariableLabelTooLong { max, actual, .. } => {
write!(
f,
"variable label exceeds {} bytes (has {} bytes)",
max, actual
)?;
}
Self::NumericWrongLength {
variable,
expected,
actual,
} => {
write!(
f,
"numeric variable '{}' must have length {} (has {})",
variable, expected, actual
)?;
}
Self::CharacterLengthTooShort {
variable,
min,
actual,
} => {
write!(
f,
"character variable '{}' must have length >= {} (has {})",
variable, min, actual
)?;
}
Self::CharacterLengthTooLong {
variable,
max,
actual,
} => {
write!(
f,
"character variable '{}' must have length <= {} (has {})",
variable, max, actual
)?;
}
Self::RowLenInconsistent { recorded, computed } => {
write!(
f,
"row_len inconsistency: recorded {} but computed {}",
recorded, computed
)?;
}
Self::DatasetNamePatternMismatch {
dataset,
agency,
pattern,
} => {
write!(
f,
"dataset name '{}' does not match {} required pattern '{}'",
dataset, agency, pattern
)?;
}
Self::VariableNamePatternMismatch {
variable,
agency,
pattern,
} => {
write!(
f,
"variable name '{}' does not match {} required pattern '{}'",
variable, agency, pattern
)?;
}
Self::DatasetNameFileStemMismatch { dataset, stem } => {
write!(
f,
"dataset name '{}' does not match file stem '{}'",
dataset, stem
)?;
}
Self::NonAsciiDatasetName { dataset } => {
write!(
f,
"dataset name '{}' contains non-ASCII characters",
dataset
)?;
}
Self::NonAsciiVariableName { variable } => {
write!(
f,
"variable name '{}' contains non-ASCII characters",
variable
)?;
}
Self::NonAsciiDatasetLabel { .. } => {
write!(f, "dataset label contains non-ASCII characters")?;
}
Self::NonAsciiVariableLabel { variable } => {
write!(
f,
"variable '{}' label contains non-ASCII characters",
variable
)?;
}
Self::AgencyLabelTooLong {
name,
is_dataset,
max,
actual,
} => {
let kind = if *is_dataset { "dataset" } else { "variable" };
write!(
f,
"{} '{}' label exceeds {} bytes (has {} bytes)",
kind, name, max, actual
)?;
}
Self::CharacterValueLengthExceeded {
variable,
length,
agency,
max,
} => {
write!(
f,
"character variable '{}' length {} exceeds {} policy limit of {} bytes",
variable, length, agency, max
)?;
}
Self::MultiByteLabelNearLimit {
name,
is_dataset,
byte_count,
max_bytes,
char_count,
} => {
let kind = if *is_dataset { "dataset" } else { "variable" };
write!(
f,
"{} '{}' label uses {} of {} bytes ({} characters) - approaching limit with multi-byte characters",
kind, name, byte_count, max_bytes, char_count
)?;
}
Self::MissingVariableLabel { variable } => {
write!(
f,
"variable '{}' is missing a label (recommended for FDA submissions)",
variable
)?;
}
Self::MissingDatasetLabel { dataset } => {
write!(
f,
"dataset '{}' is missing a label (recommended for FDA submissions)",
dataset
)?;
}
Self::InvalidFormatSyntax {
variable,
format,
reason,
} => {
write!(
f,
"variable '{}' has invalid format '{}': {}",
variable, format, reason
)?;
}
}
if let Some(ref target) = self.target() {
write!(f, " ({})", target)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Severity {
Info,
Warning,
Error,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "INFO"),
Self::Warning => write!(f, "WARN"),
Self::Error => write!(f, "ERROR"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(dead_code)]
pub enum Target {
Dataset(String),
Variable(String),
File(PathBuf),
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dataset(name) => write!(f, "dataset: {}", name),
Self::Variable(name) => write!(f, "variable: {}", name),
Self::File(path) => write!(f, "file: {}", path.display()),
}
}
}
#[allow(dead_code)]
pub trait IssueCollection {
fn has_errors(&self) -> bool;
fn has_warnings(&self) -> bool;
fn errors(&self) -> impl Iterator<Item = &Issue>;
fn warnings(&self) -> impl Iterator<Item = &Issue>;
}
impl IssueCollection for [Issue] {
fn has_errors(&self) -> bool {
self.iter().any(Issue::is_error)
}
fn has_warnings(&self) -> bool {
self.iter().any(Issue::is_warning)
}
fn errors(&self) -> impl Iterator<Item = &Issue> {
self.iter().filter(|i| i.is_error())
}
fn warnings(&self) -> impl Iterator<Item = &Issue> {
self.iter().filter(|i| i.is_warning())
}
}
impl IssueCollection for Vec<Issue> {
fn has_errors(&self) -> bool {
self.as_slice().has_errors()
}
fn has_warnings(&self) -> bool {
self.as_slice().has_warnings()
}
fn errors(&self) -> impl Iterator<Item = &Issue> {
self.as_slice().errors()
}
fn warnings(&self) -> impl Iterator<Item = &Issue> {
self.as_slice().warnings()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issue_display() {
let issue = Issue::VariableNameTooLong {
variable: "TOOLONGVARIABLENAME".into(),
max: 8,
actual: 19,
};
let display = format!("{}", issue);
assert!(display.contains("ERROR"));
assert!(display.contains("TOOLONGVARIABLENAME"));
assert!(display.contains("exceeds 8 bytes"));
}
#[test]
fn test_issue_collection() {
let issues = vec![
Issue::DatasetNameTooLong {
dataset: "TOOLONG".into(),
max: 8,
actual: 10,
},
Issue::CharacterValueLengthExceeded {
variable: "VAR".into(),
length: 300,
agency: "FDA",
max: 200,
},
];
assert!(issues.has_errors());
assert!(issues.has_warnings());
assert_eq!(issues.errors().count(), 1);
assert_eq!(issues.warnings().count(), 1);
}
}