Skip to main content

solari/
code.rs

1//! `code` namespace (`code.run`): run code in a stateful kernel and flatten the
2//! per-result `chart` fields into a top-level `charts` array. Mirrors `runCode`
3//! in `handle.ts`.
4
5use serde::Deserialize;
6use serde_json::{Map, Value};
7
8use crate::client::Sandbox;
9use crate::error::SolariError;
10use crate::types::{CodeResultItem, RunCodeResult};
11
12/// Options for `code.run`.
13#[derive(Default, Clone)]
14pub struct RunCodeOptions {
15    /// Language (defaults server-side to python).
16    pub language: Option<String>,
17    /// Stateful kernel context id (REPL persistence across calls).
18    pub context_id: Option<String>,
19}
20
21#[derive(Default, Deserialize)]
22struct RawRunCode {
23    #[serde(default)]
24    results: Option<Vec<CodeResultItem>>,
25    #[serde(default)]
26    error: Option<Value>,
27}
28
29/// Build a [`RunCodeResult`] from a raw `code.run` reply, flattening every
30/// `results[i].chart` into the top-level `charts` array.
31pub(crate) fn build_run_code_result(v: Value) -> RunCodeResult {
32    let raw: RawRunCode = serde_json::from_value(v).unwrap_or_default();
33    let results = raw.results.unwrap_or_default();
34    let charts = results.iter().filter_map(|i| i.chart.clone()).collect();
35    RunCodeResult {
36        results,
37        error: raw.error,
38        charts,
39    }
40}
41
42/// The ergonomic `code` accessor returned by `Sandbox::code()`.
43pub struct Code<'a> {
44    pub(crate) sb: &'a Sandbox,
45}
46
47impl<'a> Code<'a> {
48    /// Run `code` in a stateful kernel and return rich results + flattened charts.
49    pub async fn run(&self, code: &str, opts: RunCodeOptions) -> Result<RunCodeResult, SolariError> {
50        self.sb.channel().connect().await?;
51        let mut p = Map::new();
52        p.insert("code".into(), Value::String(code.to_string()));
53        if let Some(lang) = &opts.language {
54            p.insert("language".into(), Value::String(lang.clone()));
55        }
56        if let Some(ctx) = &opts.context_id {
57            p.insert("contextId".into(), Value::String(ctx.clone()));
58        }
59        let raw = self.sb.channel().call("code.run", Value::Object(p)).await?;
60        Ok(build_run_code_result(raw))
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::types::ChartType;
68
69    #[test]
70    fn flattens_result_charts_into_top_level() {
71        let reply = serde_json::json!({
72            "results": [
73                { "type": "stdout", "text": "hi" },
74                { "type": "result", "png": "AAAA",
75                  "chart": { "type": "line", "title": "T",
76                             "elements": [{"points": [[1,2]]}] } },
77                { "type": "result", "chart": { "type": "bar" } },
78                { "type": "result", "html": "<b/>" }
79            ],
80            "error": null
81        });
82        let out = build_run_code_result(reply);
83        assert_eq!(out.results.len(), 4);
84        assert_eq!(out.charts.len(), 2);
85        assert_eq!(out.charts[0].chart_type, ChartType::Line);
86        assert_eq!(out.charts[0].title.as_deref(), Some("T"));
87        // `elements` stays as raw JSON.
88        assert!(out.charts[0].elements.is_some());
89        assert_eq!(out.charts[1].chart_type, ChartType::Bar);
90    }
91
92    #[test]
93    fn unknown_chart_type_does_not_fail() {
94        let reply = serde_json::json!({
95            "results": [ { "type": "result", "chart": { "type": "sankey" } } ]
96        });
97        let out = build_run_code_result(reply);
98        assert_eq!(out.charts.len(), 1);
99        assert_eq!(out.charts[0].chart_type, ChartType::Unknown);
100    }
101}