mod rules;
use std::path::Path;
pub use rules::Rule;
use crate::schema::DatasetSchema;
use crate::validate::Issue;
use crate::xpt::XptVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Agency {
FDA,
PMDA,
NMPA,
}
impl Agency {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::FDA => "FDA",
Self::PMDA => "PMDA",
Self::NMPA => "NMPA",
}
}
#[must_use]
pub const fn xpt_version(self) -> XptVersion {
XptVersion::V5
}
#[must_use]
pub const fn max_dataset_name_bytes(self) -> usize {
8
}
#[must_use]
pub const fn max_variable_name_bytes(self) -> usize {
8
}
#[must_use]
pub const fn max_label_bytes(self) -> usize {
40
}
#[must_use]
pub const fn max_character_value_bytes(self) -> usize {
200
}
#[must_use]
pub const fn max_file_size_gb(self) -> f64 {
5.0
}
#[must_use]
pub const fn requires_ascii_names(self) -> bool {
true
}
#[must_use]
pub const fn requires_ascii_labels(self) -> bool {
match self {
Self::FDA => true,
Self::PMDA | Self::NMPA => false,
}
}
#[must_use]
pub const fn requires_ascii_values(self) -> bool {
match self {
Self::FDA => true,
Self::PMDA | Self::NMPA => false,
}
}
#[must_use]
pub const fn requires_dataset_name_matches_file_stem(self) -> bool {
true
}
#[must_use]
pub fn rules(self) -> Vec<Rule> {
let mut rules = vec![
Rule::RequireAsciiNames,
Rule::DatasetNameMaxBytes(self.max_dataset_name_bytes()),
Rule::VariableNameMaxBytes(self.max_variable_name_bytes()),
Rule::LabelMaxBytes(self.max_label_bytes()),
Rule::CharacterValueMaxBytes(self.max_character_value_bytes()),
Rule::DatasetNameMatchesFileStem,
Rule::dataset_name_pattern(r"^[A-Z][A-Z0-9]{0,7}$"),
Rule::variable_name_pattern(r"^[A-Z_][A-Z0-9_]{0,7}$"),
Rule::MaxFileSizeGb(self.max_file_size_gb()),
];
if self.requires_ascii_labels() {
rules.push(Rule::RequireAsciiLabels);
}
if self.requires_ascii_values() {
rules.push(Rule::RequireAsciiCharacterValues);
}
rules
}
#[must_use]
pub(crate) fn validate(self, plan: &DatasetSchema, file_path: Option<&Path>) -> Vec<Issue> {
let mut issues = Vec::new();
let agency_name = self.name();
for rule in self.rules() {
issues.extend(rule.validate(plan, file_path, agency_name));
}
issues
}
}
impl std::fmt::Display for Agency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::plan::VariableSpec;
use crate::validate::Severity;
#[test]
fn test_agency_properties() {
assert_eq!(Agency::FDA.name(), "FDA");
assert_eq!(Agency::FDA.max_dataset_name_bytes(), 8);
assert_eq!(Agency::FDA.max_variable_name_bytes(), 8);
assert_eq!(Agency::FDA.max_label_bytes(), 40);
assert_eq!(Agency::FDA.max_character_value_bytes(), 200);
}
#[test]
fn test_agency_rules() {
let rules = Agency::FDA.rules();
assert!(!rules.is_empty());
assert!(rules.iter().any(|r| matches!(r, Rule::RequireAsciiNames)));
assert!(rules.iter().any(|r| matches!(r, Rule::LabelMaxBytes(40))));
}
#[test]
fn test_agency_validation_valid() {
let mut plan = DatasetSchema::new("AE");
plan.variables = vec![
VariableSpec::numeric("AESEQ"),
VariableSpec::character("USUBJID", 20),
];
plan.recalculate_positions();
let issues = Agency::FDA.validate(&plan, None);
assert!(!issues.iter().any(|i| i.severity() == Severity::Error));
}
#[test]
fn test_agency_validation_non_ascii() {
let mut plan = DatasetSchema::new("AÉ");
plan.variables = vec![VariableSpec::numeric("AESEQ")];
plan.recalculate_positions();
let issues = Agency::FDA.validate(&plan, None);
assert!(
issues
.iter()
.any(|i| matches!(i, Issue::NonAsciiDatasetName { .. }))
);
}
#[test]
fn test_agency_display() {
assert_eq!(format!("{}", Agency::FDA), "FDA");
assert_eq!(format!("{}", Agency::PMDA), "PMDA");
assert_eq!(format!("{}", Agency::NMPA), "NMPA");
}
#[test]
fn test_agency_specific_ascii_requirements() {
assert!(Agency::FDA.requires_ascii_names());
assert!(Agency::FDA.requires_ascii_labels());
assert!(Agency::FDA.requires_ascii_values());
assert!(Agency::PMDA.requires_ascii_names());
assert!(!Agency::PMDA.requires_ascii_labels());
assert!(!Agency::PMDA.requires_ascii_values());
assert!(Agency::NMPA.requires_ascii_names());
assert!(!Agency::NMPA.requires_ascii_labels());
assert!(!Agency::NMPA.requires_ascii_values());
}
#[test]
fn test_agency_rules_differ() {
let fda_rules = Agency::FDA.rules();
let pmda_rules = Agency::PMDA.rules();
assert!(
fda_rules
.iter()
.any(|r| matches!(r, Rule::RequireAsciiLabels))
);
assert!(
!pmda_rules
.iter()
.any(|r| matches!(r, Rule::RequireAsciiLabels))
);
}
#[test]
fn test_pmda_allows_japanese_labels() {
let mut plan = DatasetSchema::new("AE");
plan.dataset_label = Some("有害事象".to_string()); plan.variables = vec![VariableSpec::numeric("AESEQ")];
plan.recalculate_positions();
let issues = Agency::PMDA.validate(&plan, None);
assert!(!issues.iter().any(|i| i.severity() == Severity::Error));
}
#[test]
fn test_nmpa_allows_chinese_labels() {
let mut plan = DatasetSchema::new("AE");
plan.dataset_label = Some("不良事件".to_string()); plan.variables = vec![VariableSpec::numeric("AESEQ")];
plan.recalculate_positions();
let issues = Agency::NMPA.validate(&plan, None);
assert!(!issues.iter().any(|i| i.severity() == Severity::Error));
}
#[test]
fn test_fda_rejects_non_ascii_labels() {
let mut plan = DatasetSchema::new("AE");
plan.dataset_label = Some("Événements".to_string()); plan.variables = vec![VariableSpec::numeric("AESEQ")];
plan.recalculate_positions();
let issues = Agency::FDA.validate(&plan, None);
assert!(
issues
.iter()
.any(|i| matches!(i, Issue::NonAsciiDatasetLabel { .. }))
);
}
#[test]
fn test_multibyte_label_warning() {
let mut plan = DatasetSchema::new("AE");
plan.dataset_label = Some("有害事象データセット詳細".to_string());
plan.variables = vec![VariableSpec::numeric("AESEQ")];
plan.recalculate_positions();
let issues = Agency::PMDA.validate(&plan, None);
assert!(
issues
.iter()
.any(|i| matches!(i, Issue::MultiByteLabelNearLimit { .. }))
);
}
}