use std::path::Path;
use super::{ErrorKind, ValidationContext};
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default)]
pub struct FileManifest {
pub config_json: bool,
pub penalties_json: bool,
pub stages_json: bool,
pub initial_conditions_json: bool,
pub system_buses_json: bool,
pub system_lines_json: bool,
pub system_hydros_json: bool,
pub system_thermals_json: bool,
pub system_non_controllable_sources_json: bool,
pub system_pumping_stations_json: bool,
pub system_energy_contracts_json: bool,
pub system_hydro_geometry_parquet: bool,
pub system_hydro_production_models_json: bool,
pub system_fpha_hyperplanes_parquet: bool,
pub scenarios_inflow_history_parquet: bool,
pub scenarios_inflow_seasonal_stats_parquet: bool,
pub scenarios_inflow_ar_coefficients_parquet: bool,
pub scenarios_external_scenarios_parquet: bool,
pub scenarios_load_seasonal_stats_parquet: bool,
pub scenarios_load_factors_json: bool,
pub scenarios_correlation_json: bool,
pub scenarios_noise_openings_parquet: bool,
pub constraints_thermal_bounds_parquet: bool,
pub constraints_hydro_bounds_parquet: bool,
pub constraints_line_bounds_parquet: bool,
pub constraints_pumping_bounds_parquet: bool,
pub constraints_contract_bounds_parquet: bool,
pub constraints_exchange_factors_json: bool,
pub constraints_generic_constraints_json: bool,
pub constraints_generic_constraint_bounds_parquet: bool,
pub constraints_penalty_overrides_bus_parquet: bool,
pub constraints_penalty_overrides_line_parquet: bool,
pub constraints_penalty_overrides_hydro_parquet: bool,
pub constraints_penalty_overrides_ncs_parquet: bool,
}
struct FileEntry {
relative: &'static str,
required: bool,
}
const FILE_ENTRIES: &[FileEntry] = &[
FileEntry {
relative: "config.json",
required: true,
},
FileEntry {
relative: "penalties.json",
required: true,
},
FileEntry {
relative: "stages.json",
required: true,
},
FileEntry {
relative: "initial_conditions.json",
required: true,
},
FileEntry {
relative: "system/buses.json",
required: true,
},
FileEntry {
relative: "system/lines.json",
required: true,
},
FileEntry {
relative: "system/hydros.json",
required: true,
},
FileEntry {
relative: "system/thermals.json",
required: true,
},
FileEntry {
relative: "system/non_controllable_sources.json",
required: false,
},
FileEntry {
relative: "system/pumping_stations.json",
required: false,
},
FileEntry {
relative: "system/energy_contracts.json",
required: false,
},
FileEntry {
relative: "system/hydro_geometry.parquet",
required: false,
},
FileEntry {
relative: "system/hydro_production_models.json",
required: false,
},
FileEntry {
relative: "system/fpha_hyperplanes.parquet",
required: false,
},
FileEntry {
relative: "scenarios/inflow_history.parquet",
required: false,
},
FileEntry {
relative: "scenarios/inflow_seasonal_stats.parquet",
required: false,
},
FileEntry {
relative: "scenarios/inflow_ar_coefficients.parquet",
required: false,
},
FileEntry {
relative: "scenarios/external_scenarios.parquet",
required: false,
},
FileEntry {
relative: "scenarios/load_seasonal_stats.parquet",
required: false,
},
FileEntry {
relative: "scenarios/load_factors.json",
required: false,
},
FileEntry {
relative: "scenarios/correlation.json",
required: false,
},
FileEntry {
relative: "scenarios/noise_openings.parquet",
required: false,
},
FileEntry {
relative: "constraints/thermal_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/hydro_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/line_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/pumping_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/contract_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/exchange_factors.json",
required: false,
},
FileEntry {
relative: "constraints/generic_constraints.json",
required: false,
},
FileEntry {
relative: "constraints/generic_constraint_bounds.parquet",
required: false,
},
FileEntry {
relative: "constraints/penalty_overrides_bus.parquet",
required: false,
},
FileEntry {
relative: "constraints/penalty_overrides_line.parquet",
required: false,
},
FileEntry {
relative: "constraints/penalty_overrides_hydro.parquet",
required: false,
},
FileEntry {
relative: "constraints/penalty_overrides_ncs.parquet",
required: false,
},
];
#[must_use]
pub fn validate_structure(case_root: &Path, ctx: &mut ValidationContext) -> FileManifest {
let mut manifest = FileManifest::default();
for (entry, present) in FILE_ENTRIES.iter().zip(manifest_fields_mut(&mut manifest)) {
let full_path = case_root.join(entry.relative);
if full_path.exists() {
*present = true;
} else if entry.required {
ctx.add_error(
ErrorKind::FileNotFound,
entry.relative,
None::<&str>,
format!(
"required file '{}' not found in case directory",
entry.relative
),
);
}
}
manifest
}
fn manifest_fields_mut(m: &mut FileManifest) -> [&mut bool; 34] {
[
&mut m.config_json,
&mut m.penalties_json,
&mut m.stages_json,
&mut m.initial_conditions_json,
&mut m.system_buses_json,
&mut m.system_lines_json,
&mut m.system_hydros_json,
&mut m.system_thermals_json,
&mut m.system_non_controllable_sources_json,
&mut m.system_pumping_stations_json,
&mut m.system_energy_contracts_json,
&mut m.system_hydro_geometry_parquet,
&mut m.system_hydro_production_models_json,
&mut m.system_fpha_hyperplanes_parquet,
&mut m.scenarios_inflow_history_parquet,
&mut m.scenarios_inflow_seasonal_stats_parquet,
&mut m.scenarios_inflow_ar_coefficients_parquet,
&mut m.scenarios_external_scenarios_parquet,
&mut m.scenarios_load_seasonal_stats_parquet,
&mut m.scenarios_load_factors_json,
&mut m.scenarios_correlation_json,
&mut m.scenarios_noise_openings_parquet,
&mut m.constraints_thermal_bounds_parquet,
&mut m.constraints_hydro_bounds_parquet,
&mut m.constraints_line_bounds_parquet,
&mut m.constraints_pumping_bounds_parquet,
&mut m.constraints_contract_bounds_parquet,
&mut m.constraints_exchange_factors_json,
&mut m.constraints_generic_constraints_json,
&mut m.constraints_generic_constraint_bounds_parquet,
&mut m.constraints_penalty_overrides_bus_parquet,
&mut m.constraints_penalty_overrides_line_parquet,
&mut m.constraints_penalty_overrides_hydro_parquet,
&mut m.constraints_penalty_overrides_ncs_parquet,
]
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::validation::ValidationContext;
use std::fs;
use tempfile::TempDir;
fn make_case_with_required(dir: &TempDir) {
let root = dir.path();
fs::create_dir_all(root.join("system")).unwrap();
fs::write(root.join("config.json"), b"{}").unwrap();
fs::write(root.join("penalties.json"), b"{}").unwrap();
fs::write(root.join("stages.json"), b"{}").unwrap();
fs::write(root.join("initial_conditions.json"), b"{}").unwrap();
fs::write(root.join("system/buses.json"), b"{}").unwrap();
fs::write(root.join("system/lines.json"), b"{}").unwrap();
fs::write(root.join("system/hydros.json"), b"{}").unwrap();
fs::write(root.join("system/thermals.json"), b"{}").unwrap();
}
#[test]
fn test_structural_all_required_present() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(
!ctx.has_errors(),
"should have 0 errors when all required files present, got: {:?}",
ctx.errors()
);
assert!(manifest.config_json, "config.json should be present");
assert!(manifest.penalties_json, "penalties.json should be present");
assert!(manifest.stages_json, "stages.json should be present");
assert!(
manifest.initial_conditions_json,
"initial_conditions.json should be present"
);
assert!(
manifest.system_buses_json,
"system/buses.json should be present"
);
assert!(
manifest.system_lines_json,
"system/lines.json should be present"
);
assert!(
manifest.system_hydros_json,
"system/hydros.json should be present"
);
assert!(
manifest.system_thermals_json,
"system/thermals.json should be present"
);
}
#[test]
fn test_structural_missing_required_hydros() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
fs::remove_file(dir.path().join("system/hydros.json")).unwrap();
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(
ctx.has_errors(),
"should have at least 1 error when system/hydros.json is missing"
);
assert_eq!(ctx.errors().len(), 1, "should have exactly 1 error");
let entry = &ctx.errors()[0];
assert_eq!(
entry.kind,
crate::validation::ErrorKind::FileNotFound,
"error kind should be FileNotFound"
);
assert!(
entry.file.to_string_lossy().contains("hydros.json"),
"error file should reference hydros.json, got: {}",
entry.file.display()
);
assert!(
!manifest.system_hydros_json,
"manifest.system_hydros_json should be false"
);
}
#[test]
fn test_structural_optional_absent_no_error() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(
!ctx.has_errors(),
"absent optional files should not produce errors"
);
assert!(!manifest.system_non_controllable_sources_json);
assert!(!manifest.system_hydro_geometry_parquet);
assert!(!manifest.scenarios_inflow_history_parquet);
assert!(!manifest.constraints_thermal_bounds_parquet);
assert!(!manifest.scenarios_correlation_json);
}
#[test]
fn test_structural_optional_present_in_manifest() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
let scenarios_dir = dir.path().join("scenarios");
fs::create_dir_all(&scenarios_dir).unwrap();
fs::write(scenarios_dir.join("correlation.json"), b"{}").unwrap();
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(!ctx.has_errors());
assert!(
manifest.scenarios_correlation_json,
"present optional file should be marked true in manifest"
);
}
#[test]
fn test_structural_multiple_missing_required() {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("system")).unwrap();
let mut ctx = ValidationContext::new();
let _manifest = validate_structure(dir.path(), &mut ctx);
assert!(
ctx.has_errors(),
"should have errors for all 8 missing required files"
);
assert_eq!(
ctx.errors().len(),
8,
"should have exactly 8 errors (one per required file), got: {}",
ctx.errors().len()
);
for entry in ctx.errors() {
assert_eq!(
entry.kind,
crate::validation::ErrorKind::FileNotFound,
"all errors should be FileNotFound"
);
}
}
#[test]
fn test_structural_manifest_fields_count() {
assert_eq!(
FILE_ENTRIES.len(),
34,
"FILE_ENTRIES should have exactly 34 entries"
);
let mut manifest = FileManifest::default();
let fields = manifest_fields_mut(&mut manifest);
assert_eq!(
fields.len(),
34,
"manifest_fields_mut should return exactly 34 fields"
);
}
#[test]
fn test_manifest_noise_openings_absent() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(
!ctx.has_errors(),
"absent optional file should not produce errors"
);
assert!(
!manifest.scenarios_noise_openings_parquet,
"scenarios_noise_openings_parquet should be false when file is absent"
);
}
#[test]
fn test_manifest_noise_openings_present() {
let dir = TempDir::new().unwrap();
make_case_with_required(&dir);
let scenarios_dir = dir.path().join("scenarios");
fs::create_dir_all(&scenarios_dir).unwrap();
fs::write(scenarios_dir.join("noise_openings.parquet"), b"").unwrap();
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(
!ctx.has_errors(),
"present optional file should not produce errors"
);
assert!(
manifest.scenarios_noise_openings_parquet,
"scenarios_noise_openings_parquet should be true when file is present"
);
}
}