use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use wickra_backtest_core::{BacktestReport, Candle, StrategySpec};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct BenchmarkCase {
pub id: String,
pub description: String,
pub strategy: Value,
pub dataset_ref: String,
pub expected: Value,
pub expected_hash: String,
}
fn is_hex64_lowercase(s: &str) -> bool {
s.len() == 64
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
impl BenchmarkCase {
pub fn from_json(s: &str) -> Result<Self> {
let case: Self = serde_json::from_str(s).map_err(|e| Error::BadCase(e.to_string()))?;
case.validate()?;
Ok(case)
}
pub fn from_toml(s: &str) -> Result<Self> {
let case: Self = toml::from_str(s).map_err(|e| Error::Parse(e.to_string()))?;
case.validate()?;
Ok(case)
}
pub(crate) fn validate(&self) -> Result<()> {
if self.id.is_empty() {
return Err(Error::BadCase("case id must not be empty".to_string()));
}
if self.dataset_ref.is_empty() {
return Err(Error::BadCase("dataset_ref must not be empty".to_string()));
}
if !is_hex64_lowercase(&self.expected_hash) {
return Err(Error::BadCase(format!(
"expected_hash must be 64 lowercase hex characters, got {:?}",
self.expected_hash
)));
}
if !self.expected.is_object() {
return Err(Error::BadCase(
"expected must be a JSON object (a BacktestReport)".to_string(),
));
}
if !self.strategy.is_object() {
return Err(Error::BadSpec(
"strategy must be a JSON object (a StrategySpec)".to_string(),
));
}
Ok(())
}
}