use std::path::Path;
use cobre_core::{
GenericConstraint,
entities::{Bus, EnergyContract, Hydro, Line, NonControllableSource, PumpingStation, Thermal},
initial_conditions::InitialConditions,
penalty::GlobalPenaltyDefaults,
scenario::CorrelationModel,
};
use crate::{
LoadError,
config::{Config, parse_config},
constraints::{
BusPenaltyOverrideRow, ContractBoundsRow, ExchangeFactorEntry, GenericConstraintBoundsRow,
HydroBoundsRow, HydroPenaltyOverrideRow, LineBoundsRow, LinePenaltyOverrideRow,
NcsPenaltyOverrideRow, PumpingBoundsRow, ThermalBoundsRow, load_contract_bounds,
load_exchange_factors, load_generic_constraint_bounds, load_generic_constraints,
load_hydro_bounds, load_line_bounds, load_penalty_overrides_bus,
load_penalty_overrides_hydro, load_penalty_overrides_line, load_penalty_overrides_ncs,
load_pumping_bounds, load_thermal_bounds,
},
extensions::{
FphaHyperplaneRow, HydroGeometryRow, ProductionModelConfig, load_fpha_hyperplanes,
load_production_models, parse_hydro_geometry,
},
initial_conditions::parse_initial_conditions,
penalties::parse_penalties,
scenarios::{
ExternalScenarioRow, InflowArCoefficientRow, InflowHistoryRow, InflowSeasonalStatsRow,
LoadFactorEntry, LoadSeasonalStatsRow, load_correlation, load_external_scenarios,
load_inflow_ar_coefficients, load_inflow_history, load_inflow_seasonal_stats,
load_load_factors, load_load_seasonal_stats,
},
stages::StagesData,
system::{
load_energy_contracts, load_non_controllable_sources, load_pumping_stations, parse_buses,
parse_hydros, parse_lines, parse_thermals,
},
validation::{ErrorKind, ValidationContext, structural::FileManifest},
};
pub(crate) struct ParsedData {
#[allow(dead_code)]
pub(crate) config: Config,
#[allow(dead_code)]
pub(crate) penalties: GlobalPenaltyDefaults,
pub(crate) stages: StagesData,
pub(crate) initial_conditions: InitialConditions,
pub(crate) buses: Vec<Bus>,
pub(crate) thermals: Vec<Thermal>,
pub(crate) hydros: Vec<Hydro>,
pub(crate) lines: Vec<Line>,
pub(crate) non_controllable_sources: Vec<NonControllableSource>,
pub(crate) pumping_stations: Vec<PumpingStation>,
pub(crate) energy_contracts: Vec<EnergyContract>,
pub(crate) hydro_geometry: Vec<HydroGeometryRow>,
pub(crate) production_models: Vec<ProductionModelConfig>,
pub(crate) fpha_hyperplanes: Vec<FphaHyperplaneRow>,
pub(crate) inflow_history: Vec<InflowHistoryRow>,
pub(crate) inflow_seasonal_stats: Vec<InflowSeasonalStatsRow>,
pub(crate) inflow_ar_coefficients: Vec<InflowArCoefficientRow>,
pub(crate) external_scenarios: Vec<ExternalScenarioRow>,
pub(crate) load_seasonal_stats: Vec<LoadSeasonalStatsRow>,
#[allow(dead_code)]
pub(crate) load_factors: Vec<LoadFactorEntry>,
pub(crate) correlation: Option<CorrelationModel>,
pub(crate) thermal_bounds: Vec<ThermalBoundsRow>,
pub(crate) hydro_bounds: Vec<HydroBoundsRow>,
pub(crate) line_bounds: Vec<LineBoundsRow>,
pub(crate) pumping_bounds: Vec<PumpingBoundsRow>,
pub(crate) contract_bounds: Vec<ContractBoundsRow>,
#[allow(dead_code)]
pub(crate) exchange_factors: Vec<ExchangeFactorEntry>,
pub(crate) generic_constraints: Vec<GenericConstraint>,
pub(crate) generic_constraint_bounds: Vec<GenericConstraintBoundsRow>,
pub(crate) penalty_overrides_bus: Vec<BusPenaltyOverrideRow>,
pub(crate) penalty_overrides_line: Vec<LinePenaltyOverrideRow>,
pub(crate) penalty_overrides_hydro: Vec<HydroPenaltyOverrideRow>,
pub(crate) penalty_overrides_ncs: Vec<NcsPenaltyOverrideRow>,
}
fn map_load_error(err: &LoadError, relative_path: &str, ctx: &mut ValidationContext) {
match err {
LoadError::IoError { .. } => {
ctx.add_error(
ErrorKind::FileNotFound,
relative_path,
None::<&str>,
err.to_string(),
);
}
LoadError::ParseError { .. } => {
ctx.add_error(
ErrorKind::ParseError,
relative_path,
None::<&str>,
err.to_string(),
);
}
LoadError::SchemaError { .. } => {
ctx.add_error(
ErrorKind::SchemaViolation,
relative_path,
None::<&str>,
err.to_string(),
);
}
_ => {
ctx.add_error(
ErrorKind::SchemaViolation,
relative_path,
None::<&str>,
err.to_string(),
);
}
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub(crate) fn validate_schema(
case_root: &Path,
manifest: &FileManifest,
ctx: &mut ValidationContext,
) -> Option<ParsedData> {
let error_count_before = ctx.error_count();
let config = parse_or_error(
parse_config(&case_root.join("config.json")),
"config.json",
ctx,
);
let penalties = parse_or_error(
parse_penalties(&case_root.join("penalties.json")),
"penalties.json",
ctx,
);
let stages = parse_or_error(
crate::stages::parse_stages(&case_root.join("stages.json")),
"stages.json",
ctx,
);
let initial_conditions = parse_or_error(
parse_initial_conditions(&case_root.join("initial_conditions.json")),
"initial_conditions.json",
ctx,
);
let sentinel = penalties.clone().unwrap_or_else(sentinel_penalties);
let buses = parse_or_error(
parse_buses(&case_root.join("system/buses.json"), &sentinel),
"system/buses.json",
ctx,
);
let lines = parse_or_error(
parse_lines(&case_root.join("system/lines.json"), &sentinel),
"system/lines.json",
ctx,
);
let hydros = parse_or_error(
parse_hydros(&case_root.join("system/hydros.json"), &sentinel),
"system/hydros.json",
ctx,
);
let thermals = parse_or_error(
parse_thermals(&case_root.join("system/thermals.json")),
"system/thermals.json",
ctx,
);
let non_controllable_sources = optional_or_error(
manifest.system_non_controllable_sources_json,
|| {
load_non_controllable_sources(
Some(&case_root.join("system/non_controllable_sources.json")),
&sentinel,
)
},
Vec::new,
"system/non_controllable_sources.json",
ctx,
);
let pumping_stations = optional_or_error(
manifest.system_pumping_stations_json,
|| load_pumping_stations(Some(&case_root.join("system/pumping_stations.json"))),
Vec::new,
"system/pumping_stations.json",
ctx,
);
let energy_contracts = optional_or_error(
manifest.system_energy_contracts_json,
|| load_energy_contracts(Some(&case_root.join("system/energy_contracts.json"))),
Vec::new,
"system/energy_contracts.json",
ctx,
);
let hydro_geometry = optional_or_error(
manifest.system_hydro_geometry_parquet,
|| parse_hydro_geometry(&case_root.join("system/hydro_geometry.parquet")),
Vec::new,
"system/hydro_geometry.parquet",
ctx,
);
let production_models = optional_or_error(
manifest.system_hydro_production_models_json,
|| load_production_models(Some(&case_root.join("system/hydro_production_models.json"))),
Vec::new,
"system/hydro_production_models.json",
ctx,
);
let fpha_hyperplanes = optional_or_error(
manifest.system_fpha_hyperplanes_parquet,
|| load_fpha_hyperplanes(Some(&case_root.join("system/fpha_hyperplanes.parquet"))),
Vec::new,
"system/fpha_hyperplanes.parquet",
ctx,
);
let inflow_history = optional_or_error(
manifest.scenarios_inflow_history_parquet,
|| load_inflow_history(Some(&case_root.join("scenarios/inflow_history.parquet"))),
Vec::new,
"scenarios/inflow_history.parquet",
ctx,
);
let inflow_seasonal_stats = optional_or_error(
manifest.scenarios_inflow_seasonal_stats_parquet,
|| {
load_inflow_seasonal_stats(Some(
&case_root.join("scenarios/inflow_seasonal_stats.parquet"),
))
},
Vec::new,
"scenarios/inflow_seasonal_stats.parquet",
ctx,
);
let inflow_ar_coefficients = optional_or_error(
manifest.scenarios_inflow_ar_coefficients_parquet,
|| {
load_inflow_ar_coefficients(Some(
&case_root.join("scenarios/inflow_ar_coefficients.parquet"),
))
},
Vec::new,
"scenarios/inflow_ar_coefficients.parquet",
ctx,
);
let external_scenarios = optional_or_error(
manifest.scenarios_external_scenarios_parquet,
|| {
load_external_scenarios(Some(
&case_root.join("scenarios/external_scenarios.parquet"),
))
},
Vec::new,
"scenarios/external_scenarios.parquet",
ctx,
);
let load_seasonal_stats = optional_or_error(
manifest.scenarios_load_seasonal_stats_parquet,
|| {
load_load_seasonal_stats(Some(
&case_root.join("scenarios/load_seasonal_stats.parquet"),
))
},
Vec::new,
"scenarios/load_seasonal_stats.parquet",
ctx,
);
let load_factors = optional_or_error(
manifest.scenarios_load_factors_json,
|| load_load_factors(Some(&case_root.join("scenarios/load_factors.json"))),
Vec::new,
"scenarios/load_factors.json",
ctx,
);
let correlation: Option<CorrelationModel> = if manifest.scenarios_correlation_json {
match load_correlation(Some(&case_root.join("scenarios/correlation.json"))) {
Ok(model) => Some(model),
Err(ref err) => {
map_load_error(err, "scenarios/correlation.json", ctx);
None
}
}
} else {
None
};
let thermal_bounds = optional_or_error(
manifest.constraints_thermal_bounds_parquet,
|| load_thermal_bounds(Some(&case_root.join("constraints/thermal_bounds.parquet"))),
Vec::new,
"constraints/thermal_bounds.parquet",
ctx,
);
let hydro_bounds = optional_or_error(
manifest.constraints_hydro_bounds_parquet,
|| load_hydro_bounds(Some(&case_root.join("constraints/hydro_bounds.parquet"))),
Vec::new,
"constraints/hydro_bounds.parquet",
ctx,
);
let line_bounds = optional_or_error(
manifest.constraints_line_bounds_parquet,
|| load_line_bounds(Some(&case_root.join("constraints/line_bounds.parquet"))),
Vec::new,
"constraints/line_bounds.parquet",
ctx,
);
let pumping_bounds = optional_or_error(
manifest.constraints_pumping_bounds_parquet,
|| load_pumping_bounds(Some(&case_root.join("constraints/pumping_bounds.parquet"))),
Vec::new,
"constraints/pumping_bounds.parquet",
ctx,
);
let contract_bounds = optional_or_error(
manifest.constraints_contract_bounds_parquet,
|| load_contract_bounds(Some(&case_root.join("constraints/contract_bounds.parquet"))),
Vec::new,
"constraints/contract_bounds.parquet",
ctx,
);
let exchange_factors = optional_or_error(
manifest.constraints_exchange_factors_json,
|| load_exchange_factors(Some(&case_root.join("constraints/exchange_factors.json"))),
Vec::new,
"constraints/exchange_factors.json",
ctx,
);
let generic_constraints = optional_or_error(
manifest.constraints_generic_constraints_json,
|| {
load_generic_constraints(Some(
&case_root.join("constraints/generic_constraints.json"),
))
},
Vec::new,
"constraints/generic_constraints.json",
ctx,
);
let generic_constraint_bounds = optional_or_error(
manifest.constraints_generic_constraint_bounds_parquet,
|| {
load_generic_constraint_bounds(Some(
&case_root.join("constraints/generic_constraint_bounds.parquet"),
))
},
Vec::new,
"constraints/generic_constraint_bounds.parquet",
ctx,
);
let penalty_overrides_bus = optional_or_error(
manifest.constraints_penalty_overrides_bus_parquet,
|| {
load_penalty_overrides_bus(Some(
&case_root.join("constraints/penalty_overrides_bus.parquet"),
))
},
Vec::new,
"constraints/penalty_overrides_bus.parquet",
ctx,
);
let penalty_overrides_line = optional_or_error(
manifest.constraints_penalty_overrides_line_parquet,
|| {
load_penalty_overrides_line(Some(
&case_root.join("constraints/penalty_overrides_line.parquet"),
))
},
Vec::new,
"constraints/penalty_overrides_line.parquet",
ctx,
);
let penalty_overrides_hydro = optional_or_error(
manifest.constraints_penalty_overrides_hydro_parquet,
|| {
load_penalty_overrides_hydro(Some(
&case_root.join("constraints/penalty_overrides_hydro.parquet"),
))
},
Vec::new,
"constraints/penalty_overrides_hydro.parquet",
ctx,
);
let penalty_overrides_ncs = optional_or_error(
manifest.constraints_penalty_overrides_ncs_parquet,
|| {
load_penalty_overrides_ncs(Some(
&case_root.join("constraints/penalty_overrides_ncs.parquet"),
))
},
Vec::new,
"constraints/penalty_overrides_ncs.parquet",
ctx,
);
if ctx.error_count() > error_count_before {
return None;
}
let (
Some(config),
Some(penalties),
Some(stages),
Some(initial_conditions),
Some(buses),
Some(lines),
Some(hydros),
Some(thermals),
) = (
config,
penalties,
stages,
initial_conditions,
buses,
lines,
hydros,
thermals,
)
else {
return None;
};
Some(ParsedData {
config,
penalties,
stages,
initial_conditions,
buses,
thermals,
hydros,
lines,
non_controllable_sources,
pumping_stations,
energy_contracts,
hydro_geometry,
production_models,
fpha_hyperplanes,
inflow_history,
inflow_seasonal_stats,
inflow_ar_coefficients,
external_scenarios,
load_seasonal_stats,
load_factors,
correlation,
thermal_bounds,
hydro_bounds,
line_bounds,
pumping_bounds,
contract_bounds,
exchange_factors,
generic_constraints,
generic_constraint_bounds,
penalty_overrides_bus,
penalty_overrides_line,
penalty_overrides_hydro,
penalty_overrides_ncs,
})
}
fn parse_or_error<T>(
result: Result<T, LoadError>,
relative_path: &str,
ctx: &mut ValidationContext,
) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(ref err) => {
map_load_error(err, relative_path, ctx);
None
}
}
}
fn optional_or_error<T, F, D>(
present: bool,
parse_fn: F,
default_fn: D,
relative_path: &str,
ctx: &mut ValidationContext,
) -> T
where
F: FnOnce() -> Result<T, LoadError>,
D: FnOnce() -> T,
{
if present {
match parse_fn() {
Ok(value) => value,
Err(ref err) => {
map_load_error(err, relative_path, ctx);
default_fn()
}
}
} else {
default_fn()
}
}
fn sentinel_penalties() -> GlobalPenaltyDefaults {
use cobre_core::entities::{DeficitSegment, HydroPenalties};
GlobalPenaltyDefaults {
bus_deficit_segments: vec![DeficitSegment {
depth_mw: None,
cost_per_mwh: 1.0,
}],
bus_excess_cost: 1.0,
line_exchange_cost: 1.0,
hydro: HydroPenalties {
spillage_cost: 1.0,
fpha_turbined_cost: 1.0,
diversion_cost: 1.0,
storage_violation_below_cost: 1.0,
filling_target_violation_cost: 1.0,
turbined_violation_below_cost: 1.0,
outflow_violation_below_cost: 1.0,
outflow_violation_above_cost: 1.0,
generation_violation_below_cost: 1.0,
evaporation_violation_cost: 1.0,
water_withdrawal_violation_cost: 1.0,
},
ncs_curtailment_cost: 1.0,
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::expect_used
)]
mod tests {
use super::*;
use crate::validation::{ErrorKind, ValidationContext, structural::validate_structure};
use std::fs;
use tempfile::TempDir;
const VALID_CONFIG_JSON: &str = r#"{
"training": {
"forward_passes": 10,
"stopping_rules": [
{ "type": "iteration_limit", "limit": 100 }
]
}
}"#;
const VALID_PENALTIES_JSON: &str = r#"{
"bus": {
"deficit_segments": [
{ "depth_mw": 500.0, "cost": 1000.0 },
{ "depth_mw": null, "cost": 5000.0 }
],
"excess_cost": 100.0
},
"line": { "exchange_cost": 2.0 },
"hydro": {
"spillage_cost": 0.01,
"fpha_turbined_cost": 0.05,
"diversion_cost": 0.1,
"storage_violation_below_cost": 10000.0,
"filling_target_violation_cost": 50000.0,
"turbined_violation_below_cost": 500.0,
"outflow_violation_below_cost": 500.0,
"outflow_violation_above_cost": 500.0,
"generation_violation_below_cost": 1000.0,
"evaporation_violation_cost": 5000.0,
"water_withdrawal_violation_cost": 1000.0
},
"non_controllable_source": { "curtailment_cost": 0.005 }
}"#;
const VALID_STAGES_JSON: &str = r#"{
"policy_graph": {
"type": "finite_horizon",
"annual_discount_rate": 0.06,
"transitions": []
},
"scenario_source": { "sampling_scheme": "in_sample", "seed": 42 },
"stages": [
{
"id": 0,
"start_date": "2024-01-01",
"end_date": "2024-02-01",
"blocks": [{ "id": 0, "name": "FLAT", "hours": 744.0 }],
"num_scenarios": 50
}
]
}"#;
const VALID_INITIAL_CONDITIONS_JSON: &str = r#"{
"storage": [],
"filling_storage": []
}"#;
const VALID_BUSES_JSON: &str = r#"{ "buses": [{ "id": 1, "name": "BUS_1" }] }"#;
const VALID_LINES_JSON: &str = r#"{ "lines": [] }"#;
const VALID_HYDROS_JSON: &str = r#"{ "hydros": [] }"#;
const VALID_THERMALS_JSON: &str = r#"{ "thermals": [] }"#;
fn write_file(root: &Path, relative: &str, content: &str) {
let full = root.join(relative);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&full, content).unwrap();
}
fn make_valid_case(dir: &TempDir) {
let root = dir.path();
write_file(root, "config.json", VALID_CONFIG_JSON);
write_file(root, "penalties.json", VALID_PENALTIES_JSON);
write_file(root, "stages.json", VALID_STAGES_JSON);
write_file(
root,
"initial_conditions.json",
VALID_INITIAL_CONDITIONS_JSON,
);
write_file(root, "system/buses.json", VALID_BUSES_JSON);
write_file(root, "system/lines.json", VALID_LINES_JSON);
write_file(root, "system/hydros.json", VALID_HYDROS_JSON);
write_file(root, "system/thermals.json", VALID_THERMALS_JSON);
}
#[test]
fn test_valid_case_returns_some_and_no_errors() {
let dir = TempDir::new().unwrap();
make_valid_case(&dir);
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(!ctx.has_errors(), "structural validation should pass");
let data = validate_schema(dir.path(), &manifest, &mut ctx);
assert!(
data.is_some(),
"validate_schema should return Some(ParsedData) for valid case"
);
assert!(
!ctx.has_errors(),
"ctx should have no errors for a valid case, got: {:?}",
ctx.errors()
);
let data = data.unwrap();
assert_eq!(
data.buses.len(),
1,
"expected 1 bus parsed from valid buses.json"
);
assert!(
data.non_controllable_sources.is_empty(),
"non_controllable_sources should be empty when file absent"
);
assert!(
data.correlation.is_none(),
"correlation should be None when file absent"
);
}
#[test]
fn test_invalid_json_returns_none_and_parse_error() {
let dir = TempDir::new().unwrap();
make_valid_case(&dir);
write_file(dir.path(), "system/hydros.json", "{ invalid json !!!");
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(!ctx.has_errors(), "structural validation should pass");
let data = validate_schema(dir.path(), &manifest, &mut ctx);
assert!(
data.is_none(),
"validate_schema should return None when a file has invalid JSON"
);
assert!(ctx.has_errors(), "ctx should have at least one error");
let parse_errors: Vec<_> = ctx
.errors()
.into_iter()
.filter(|e| e.kind == ErrorKind::ParseError)
.collect();
assert!(
!parse_errors.is_empty(),
"ctx should have at least one ParseError entry"
);
assert!(
parse_errors
.iter()
.any(|e| e.file.to_string_lossy().contains("system/hydros.json")),
"ParseError entry should reference system/hydros.json, got: {:?}",
parse_errors
.iter()
.map(|e| e.file.display().to_string())
.collect::<Vec<_>>()
);
}
#[test]
fn test_two_invalid_files_both_errors_collected() {
let dir = TempDir::new().unwrap();
make_valid_case(&dir);
write_file(
dir.path(),
"system/buses.json",
r#"{ "buses": [{ "id": 1, "name": "A" }, { "id": 1, "name": "B" }] }"#,
);
write_file(dir.path(), "penalties.json", "{ not valid json");
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(!ctx.has_errors(), "structural validation should pass");
let data = validate_schema(dir.path(), &manifest, &mut ctx);
assert!(data.is_none(), "validate_schema should return None");
assert!(
ctx.errors().len() >= 2,
"ctx should have at least 2 errors (one per invalid file), got {} errors: {:?}",
ctx.errors().len(),
ctx.errors()
.iter()
.map(|e| format!("{:?} @ {}", e.kind, e.file.display()))
.collect::<Vec<_>>()
);
assert!(
ctx.errors()
.iter()
.any(|e| e.file.to_string_lossy().contains("penalties.json")),
"expected an error referencing penalties.json"
);
assert!(
ctx.errors()
.iter()
.any(|e| e.file.to_string_lossy().contains("system/buses.json")),
"expected an error referencing system/buses.json"
);
}
#[test]
fn test_absent_optional_file_yields_none_no_error() {
let dir = TempDir::new().unwrap();
make_valid_case(&dir);
let mut ctx = ValidationContext::new();
let manifest = validate_structure(dir.path(), &mut ctx);
assert!(!ctx.has_errors(), "structural validation should pass");
assert!(
!manifest.scenarios_correlation_json,
"manifest should show correlation.json absent"
);
let data = validate_schema(dir.path(), &manifest, &mut ctx);
assert!(
data.is_some(),
"validate_schema should return Some(ParsedData)"
);
assert!(
!ctx.has_errors(),
"no error should be added for an absent optional file"
);
assert!(
data.unwrap().correlation.is_none(),
"correlation should be None when file absent"
);
}
#[test]
fn test_map_load_error_io_error() {
let mut ctx = ValidationContext::new();
let err = LoadError::io(
"system/hydros.json",
std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
);
map_load_error(&err, "system/hydros.json", &mut ctx);
assert_eq!(ctx.errors().len(), 1);
assert_eq!(ctx.errors()[0].kind, ErrorKind::FileNotFound);
assert!(
ctx.errors()[0]
.file
.to_string_lossy()
.contains("system/hydros.json")
);
}
#[test]
fn test_map_load_error_parse_error() {
let mut ctx = ValidationContext::new();
let err = LoadError::parse("stages.json", "unexpected token");
map_load_error(&err, "stages.json", &mut ctx);
assert_eq!(ctx.errors().len(), 1);
assert_eq!(ctx.errors()[0].kind, ErrorKind::ParseError);
assert!(
ctx.errors()[0]
.file
.to_string_lossy()
.contains("stages.json")
);
}
#[test]
fn test_map_load_error_schema_error() {
let mut ctx = ValidationContext::new();
let err = LoadError::SchemaError {
path: std::path::PathBuf::from("system/buses.json"),
field: "id".to_string(),
message: "duplicate id".to_string(),
};
map_load_error(&err, "system/buses.json", &mut ctx);
assert_eq!(ctx.errors().len(), 1);
assert_eq!(ctx.errors()[0].kind, ErrorKind::SchemaViolation);
assert!(
ctx.errors()[0]
.file
.to_string_lossy()
.contains("system/buses.json")
);
}
}