use cobre_core::EntityId;
use serde::Deserialize;
use std::collections::HashSet;
use std::path::Path;
use crate::LoadError;
use crate::scenarios::BlockFactor;
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub(crate) struct RawNcsFactorsFile {
#[serde(rename = "$schema")]
_schema: Option<String>,
non_controllable_factors: Vec<RawNcsFactorEntry>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
struct RawNcsFactorEntry {
ncs_id: i32,
stage_id: i32,
block_factors: Vec<RawBlockFactor>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
struct RawBlockFactor {
block_id: i32,
factor: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NcsFactorEntry {
pub ncs_id: EntityId,
pub stage_id: i32,
pub block_factors: Vec<BlockFactor>,
}
pub fn parse_non_controllable_factors(path: &Path) -> Result<Vec<NcsFactorEntry>, LoadError> {
let raw_text = std::fs::read_to_string(path).map_err(|e| LoadError::io(path, e))?;
let raw: RawNcsFactorsFile =
serde_json::from_str(&raw_text).map_err(|e| LoadError::parse(path, e.to_string()))?;
validate_raw(&raw, path)?;
Ok(convert(raw))
}
fn validate_raw(raw: &RawNcsFactorsFile, path: &Path) -> Result<(), LoadError> {
validate_no_duplicate_entries(&raw.non_controllable_factors, path)?;
for (i, entry) in raw.non_controllable_factors.iter().enumerate() {
validate_block_factors(&entry.block_factors, i, path)?;
}
Ok(())
}
fn validate_no_duplicate_entries(
entries: &[RawNcsFactorEntry],
path: &Path,
) -> Result<(), LoadError> {
let mut seen: HashSet<(i32, i32)> = HashSet::new();
for (i, entry) in entries.iter().enumerate() {
let key = (entry.ncs_id, entry.stage_id);
if !seen.insert(key) {
return Err(LoadError::SchemaError {
path: path.to_path_buf(),
field: format!("non_controllable_factors[{i}]"),
message: format!(
"duplicate (ncs_id={}, stage_id={}) in non_controllable_factors",
entry.ncs_id, entry.stage_id
),
});
}
}
Ok(())
}
fn validate_block_factors(
block_factors: &[RawBlockFactor],
entry_idx: usize,
path: &Path,
) -> Result<(), LoadError> {
for (j, bf) in block_factors.iter().enumerate() {
if !bf.factor.is_finite() || bf.factor <= 0.0 {
return Err(LoadError::SchemaError {
path: path.to_path_buf(),
field: format!("non_controllable_factors[{entry_idx}].block_factors[{j}].factor"),
message: format!("factor must be finite and > 0.0, got {}", bf.factor),
});
}
}
Ok(())
}
fn convert(raw: RawNcsFactorsFile) -> Vec<NcsFactorEntry> {
let mut entries: Vec<NcsFactorEntry> = raw
.non_controllable_factors
.into_iter()
.map(|e| {
let mut block_factors: Vec<BlockFactor> = e
.block_factors
.into_iter()
.map(|bf| BlockFactor {
block_id: bf.block_id,
factor: bf.factor,
})
.collect();
block_factors.sort_by_key(|bf| bf.block_id);
NcsFactorEntry {
ncs_id: EntityId::from(e.ncs_id),
stage_id: e.stage_id,
block_factors,
}
})
.collect();
entries.sort_by(|a, b| {
a.ncs_id
.0
.cmp(&b.ncs_id.0)
.then_with(|| a.stage_id.cmp(&b.stage_id))
});
entries
}
#[cfg(test)]
#[allow(
clippy::doc_markdown,
clippy::panic,
clippy::too_many_lines,
clippy::unwrap_used
)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn write_json(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f
}
const VALID_JSON: &str = r#"{
"non_controllable_factors": [
{
"ncs_id": 0,
"stage_id": 0,
"block_factors": [
{ "block_id": 0, "factor": 0.6 },
{ "block_id": 1, "factor": 0.8 }
]
},
{
"ncs_id": 1,
"stage_id": 0,
"block_factors": [
{ "block_id": 0, "factor": 0.9 }
]
}
]
}"#;
#[test]
fn test_valid_2_entries() {
let tmp = write_json(VALID_JSON);
let entries = parse_non_controllable_factors(tmp.path()).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].ncs_id, EntityId::from(0));
assert_eq!(entries[0].block_factors.len(), 2);
assert!((entries[0].block_factors[0].factor - 0.6).abs() < 1e-10);
assert_eq!(entries[1].ncs_id, EntityId::from(1));
}
#[test]
fn test_sorted_output() {
let json = r#"{
"non_controllable_factors": [
{ "ncs_id": 1, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 1.0 }] },
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 0.5 }] }
]
}"#;
let tmp = write_json(json);
let entries = parse_non_controllable_factors(tmp.path()).unwrap();
assert_eq!(entries[0].ncs_id, EntityId::from(0));
assert_eq!(entries[1].ncs_id, EntityId::from(1));
}
#[test]
fn test_block_factors_sorted() {
let json = r#"{
"non_controllable_factors": [
{
"ncs_id": 0, "stage_id": 0,
"block_factors": [
{ "block_id": 2, "factor": 1.0 },
{ "block_id": 0, "factor": 0.5 },
{ "block_id": 1, "factor": 0.7 }
]
}
]
}"#;
let tmp = write_json(json);
let entries = parse_non_controllable_factors(tmp.path()).unwrap();
assert_eq!(entries[0].block_factors[0].block_id, 0);
assert_eq!(entries[0].block_factors[1].block_id, 1);
assert_eq!(entries[0].block_factors[2].block_id, 2);
}
#[test]
fn test_zero_factor_rejected() {
let json = r#"{
"non_controllable_factors": [
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 0.0 }] }
]
}"#;
let tmp = write_json(json);
let err = parse_non_controllable_factors(tmp.path()).unwrap_err();
match &err {
LoadError::SchemaError { field, .. } => {
assert!(field.contains("factor"), "field: {field}");
}
other => panic!("expected SchemaError, got: {other:?}"),
}
}
#[test]
fn test_negative_factor_rejected() {
let json = r#"{
"non_controllable_factors": [
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": -0.5 }] }
]
}"#;
let tmp = write_json(json);
let err = parse_non_controllable_factors(tmp.path()).unwrap_err();
match &err {
LoadError::SchemaError { field, .. } => {
assert!(field.contains("factor"), "field: {field}");
}
other => panic!("expected SchemaError, got: {other:?}"),
}
}
#[test]
fn test_duplicate_rejected() {
let json = r#"{
"non_controllable_factors": [
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 1.0 }] },
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 0.9 }] }
]
}"#;
let tmp = write_json(json);
let err = parse_non_controllable_factors(tmp.path()).unwrap_err();
match &err {
LoadError::SchemaError { message, .. } => {
assert!(message.contains("duplicate"), "message: {message}");
}
other => panic!("expected SchemaError, got: {other:?}"),
}
}
#[test]
fn test_empty_array() {
let json = r#"{ "non_controllable_factors": [] }"#;
let tmp = write_json(json);
let entries = parse_non_controllable_factors(tmp.path()).unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_declaration_order_invariance() {
let json_fwd = r#"{
"non_controllable_factors": [
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 1.0 }] },
{ "ncs_id": 1, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 0.5 }] }
]
}"#;
let json_rev = r#"{
"non_controllable_factors": [
{ "ncs_id": 1, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 0.5 }] },
{ "ncs_id": 0, "stage_id": 0, "block_factors": [{ "block_id": 0, "factor": 1.0 }] }
]
}"#;
let tmp_fwd = write_json(json_fwd);
let tmp_rev = write_json(json_rev);
let fwd = parse_non_controllable_factors(tmp_fwd.path()).unwrap();
let rev = parse_non_controllable_factors(tmp_rev.path()).unwrap();
assert_eq!(fwd, rev);
}
}