Skip to main content

wickra_benchmark_core/
benchmark.rs

1//! The [`Benchmark`] command-JSON handle — the FFI boundary the ten language
2//! bindings forward verbatim.
3
4use crate::case::{BenchmarkCase, Candle};
5use crate::error::{Error, Result};
6use crate::hash::canonicalize;
7use crate::runner::{run_case, run_suite_inline};
8use crate::suite::Suite;
9use serde::Deserialize;
10use serde_json::{json, Value};
11use std::collections::BTreeMap;
12use wickra_backtest_core::version as engine_version;
13
14/// A stateless benchmark handle. It carries no state — the case, suite and data
15/// arrive with each command — but is handle-shaped so the ten language bindings
16/// share the same surface as the other Wickra products.
17#[derive(Debug, Clone, Copy, Default)]
18pub struct Benchmark;
19
20#[derive(Deserialize)]
21struct RunCaseReq {
22    case: BenchmarkCase,
23    data: Vec<Candle>,
24}
25
26#[derive(Deserialize)]
27struct RunSuiteReq {
28    suite: Suite,
29    #[serde(default)]
30    datasets: BTreeMap<String, Vec<Candle>>,
31}
32
33#[derive(Deserialize)]
34struct ListCasesReq {
35    suite: Suite,
36}
37
38impl Benchmark {
39    /// Construct a benchmark handle.
40    #[must_use]
41    pub fn new() -> Self {
42        Self
43    }
44
45    /// The wickra-benchmark-core crate version.
46    #[must_use]
47    pub fn version() -> &'static str {
48        env!("CARGO_PKG_VERSION")
49    }
50
51    /// Dispatch a command envelope `{"cmd": ...}` and return a canonical JSON
52    /// string. Unknown commands and errors return an error envelope
53    /// (`{"ok":false,"error":...}`), never a panic.
54    pub fn command_json(&self, cmd_json: &str) -> Result<String> {
55        let value = dispatch(cmd_json);
56        canonicalize(&value)
57    }
58}
59
60fn dispatch(cmd_json: &str) -> Value {
61    match dispatch_inner(cmd_json) {
62        Ok(v) => v,
63        Err(e) => json!({ "ok": false, "error": e.to_string() }),
64    }
65}
66
67fn dispatch_inner(cmd_json: &str) -> Result<Value> {
68    let env: Value = serde_json::from_str(cmd_json).map_err(|e| Error::Parse(e.to_string()))?;
69    let cmd = env.get("cmd").and_then(Value::as_str).unwrap_or("");
70    match cmd {
71        "run_case" => {
72            let req: RunCaseReq =
73                serde_json::from_value(env).map_err(|e| Error::Parse(e.to_string()))?;
74            let result = run_case(&req.case, &req.data)?;
75            serde_json::to_value(result).map_err(|e| Error::BadCase(e.to_string()))
76        }
77        "run_suite" => {
78            let req: RunSuiteReq =
79                serde_json::from_value(env).map_err(|e| Error::Parse(e.to_string()))?;
80            let report = run_suite_inline(&req.suite, &req.datasets)?;
81            serde_json::to_value(report).map_err(|e| Error::BadCase(e.to_string()))
82        }
83        "list_cases" => {
84            let req: ListCasesReq =
85                serde_json::from_value(env).map_err(|e| Error::Parse(e.to_string()))?;
86            req.suite.validate()?;
87            Ok(json!({ "ids": req.suite.case_ids() }))
88        }
89        "version" => Ok(json!({
90            "version": Benchmark::version(),
91            "engine_version": engine_version(),
92        })),
93        other => Err(Error::Parse(format!("unknown cmd: {other}"))),
94    }
95}