use crate::Error;
use maned_sys as sys;
use serde::de::DeserializeOwned;
use std::ffi::CStr;
use std::ptr::NonNull;
pub struct Response {
raw: NonNull<sys::mnd_response>,
notes: Vec<String>,
}
unsafe impl Send for Response {}
impl Response {
pub(crate) fn from_raw(raw: *mut sys::mnd_response) -> Result<Response, Error> {
let raw = NonNull::new(raw).ok_or_else(|| Error::Internal {
body: "mnd_request returned NULL, which the ABI forbids".to_string(),
})?;
let notes = unsafe { parse_notes(raw.as_ref().notes) };
Ok(Response { raw, notes })
}
fn as_ref(&self) -> &sys::mnd_response {
unsafe { self.raw.as_ref() }
}
pub fn status(&self) -> u16 {
self.as_ref().status.clamp(0, u16::MAX as i32) as u16
}
pub fn body(&self) -> &[u8] {
let r = self.as_ref();
if r.body.is_null() || r.body_len == 0 {
return &[];
}
unsafe { std::slice::from_raw_parts(r.body as *const u8, r.body_len) }
}
pub fn body_str(&self) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(self.body())
}
pub fn content_type(&self) -> &str {
let r = self.as_ref();
if r.content_type.is_null() {
return "";
}
unsafe { CStr::from_ptr(r.content_type) }
.to_str()
.unwrap_or("")
}
pub fn notes(&self) -> &[String] {
&self.notes
}
pub fn json(&self) -> Result<serde_json::Value, Error> {
self.ok_or_status()?;
serde_json::from_slice(self.body()).map_err(Error::Json)
}
pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T, Error> {
self.ok_or_status()?;
serde_json::from_slice(self.body()).map_err(Error::Json)
}
pub fn tensor(&self, name: &str) -> Result<Tensor, Error> {
self.ok_or_status()?;
let root: serde_json::Value = serde_json::from_slice(self.body()).map_err(Error::Json)?;
tensor_from_run_json(&root, name)
}
pub fn decimal(&self, name: &str) -> Result<&str, Error> {
self.ok_or_status()?;
let text = self
.body_str()
.map_err(|e| Error::decode(format!("response body is not UTF-8: {e}")))?;
let outputs = json_member(text, "outputs").ok_or_else(|| {
Error::decode("response has no 'outputs' object (is this a /run.json response?)")
})?;
json_member(outputs, name)
.map(str::trim)
.ok_or_else(|| Error::decode(format!("no output named '{name}'")))
}
pub fn into_result(self) -> Result<Response, Error> {
match self.ok_or_status() {
Ok(()) => Ok(self),
Err(e) => Err(e),
}
}
fn ok_or_status(&self) -> Result<(), Error> {
let status = self.status();
if (200..300).contains(&status) {
return Ok(());
}
Err(Error::from_status(
status,
String::from_utf8_lossy(self.body()).into_owned(),
))
}
}
impl Drop for Response {
fn drop(&mut self) {
unsafe { sys::mnd_response_free(self.raw.as_ptr()) }
}
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Response")
.field("status", &self.status())
.field("content_type", &self.content_type())
.field("body_len", &self.body().len())
.field("notes", &self.notes)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tensor {
pub shape: Vec<i64>,
pub data: Vec<i64>,
}
impl Tensor {
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
fn parse_notes(p: *const std::ffi::c_char) -> Vec<String> {
if p.is_null() {
return Vec::new();
}
let raw = unsafe { CStr::from_ptr(p) };
match raw.to_str() {
Ok(s) => serde_json::from_str::<Vec<String>>(s).unwrap_or_default(),
Err(_) => Vec::new(),
}
}
pub(crate) fn tensor_from_run_json(root: &serde_json::Value, name: &str) -> Result<Tensor, Error> {
let outputs = root
.get("outputs")
.and_then(|o| o.as_object())
.ok_or_else(|| {
Error::decode("response has no 'outputs' object (is this a /run.json response?)")
})?;
let value = outputs.get(name).ok_or_else(|| {
let mut names: Vec<&str> = outputs.keys().map(String::as_str).collect();
names.sort_unstable();
Error::decode(if names.is_empty() {
format!("no output named '{name}'; the response has no outputs")
} else {
format!(
"no output named '{name}'; outputs are: {}",
names.join(", ")
)
})
})?;
let mut w = Walk {
name,
shape: Vec::new(),
data: Vec::new(),
leaf_depth: None,
};
w.walk(value, 0)?;
Ok(Tensor {
shape: w.shape,
data: w.data,
})
}
struct Walk<'a> {
name: &'a str,
shape: Vec<i64>,
data: Vec<i64>,
leaf_depth: Option<usize>,
}
impl Walk<'_> {
fn walk(&mut self, v: &serde_json::Value, depth: usize) -> Result<(), Error> {
let name = self.name;
match v {
serde_json::Value::Array(items) => {
if let Some(leaf) = self.leaf_depth {
if depth >= leaf {
return Err(Error::decode(format!(
"output '{name}' is not rectangular: an array at depth {depth} \
where a scalar belongs"
)));
}
}
if depth == self.shape.len() {
self.shape.push(items.len() as i64);
} else if self.shape[depth] != items.len() as i64 {
return Err(Error::decode(format!(
"output '{name}' is not rectangular: row of {} at depth {depth}, expected {}",
items.len(),
self.shape[depth]
)));
}
for item in items {
self.walk(item, depth + 1)?;
}
Ok(())
}
serde_json::Value::Number(n) => {
if depth != self.shape.len() {
return Err(Error::decode(format!(
"output '{name}' is not rectangular: scalar at depth {depth}, \
expected an array of {}",
self.shape[depth]
)));
}
match self.leaf_depth {
Some(leaf) if leaf != depth => {
return Err(Error::decode(format!(
"output '{name}' is not rectangular: scalar at depth {depth}, \
but other scalars sit at depth {leaf}"
)))
}
Some(_) => {}
None => self.leaf_depth = Some(depth),
}
match n.as_i64() {
Some(i) => {
self.data.push(i);
Ok(())
}
None => Err(Error::decode(format!(
"output '{name}' holds {n}, which is not an integer — the script \
runs at quantres > 0, so use decimal(\"{name}\") for the exact \
printed text"
))),
}
}
other => Err(Error::decode(format!(
"output '{name}' is not a numeric tensor (found {})",
kind_of(other)
))),
}
}
}
fn kind_of(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
}
}
pub(crate) fn json_member<'a>(obj: &'a str, key: &str) -> Option<&'a str> {
let b = obj.as_bytes();
let mut i = skip_ws(b, 0);
if b.get(i)? != &b'{' {
return None;
}
i += 1;
loop {
i = skip_ws(b, i);
match b.get(i)? {
b'}' => return None,
b'"' => {}
_ => return None,
}
let (k, next) = scan_string(b, i)?;
i = skip_ws(b, next);
if b.get(i)? != &b':' {
return None;
}
i = skip_ws(b, i + 1);
let start = i;
i = skip_value(b, i)?;
if k == key {
return obj.get(start..i);
}
i = skip_ws(b, i);
match b.get(i)? {
b',' => i += 1,
b'}' => return None,
_ => return None,
}
}
}
fn skip_ws(b: &[u8], mut i: usize) -> usize {
while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\n' | b'\r') {
i += 1;
}
i
}
fn scan_string(b: &[u8], i: usize) -> Option<(&str, usize)> {
let mut j = i + 1;
while j < b.len() {
match b[j] {
b'\\' => j += 2,
b'"' => {
let s = std::str::from_utf8(b.get(i + 1..j)?).ok()?;
return Some((s, j + 1));
}
_ => j += 1,
}
}
None
}
fn skip_value(b: &[u8], i: usize) -> Option<usize> {
match *b.get(i)? {
b'"' => Some(scan_string(b, i)?.1),
b'{' | b'[' => {
let mut depth = 0usize;
let mut j = i;
while j < b.len() {
match b[j] {
b'"' => j = scan_string(b, j)?.1,
b'{' | b'[' => {
depth += 1;
j += 1;
}
b'}' | b']' => {
depth -= 1;
j += 1;
if depth == 0 {
return Some(j);
}
}
_ => j += 1,
}
}
None
}
_ => {
let mut j = i;
while j < b.len() && !matches!(b[j], b',' | b'}' | b']' | b' ' | b'\t' | b'\n' | b'\r')
{
j += 1;
}
(j > i).then_some(j)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn tensor(v: serde_json::Value, name: &str) -> Result<Tensor, Error> {
tensor_from_run_json(&json!({ "outputs": v, "notes": [] }), name)
}
#[test]
fn parses_a_matrix() {
let t = tensor(json!({ "a": [[7, 9], [2, 4]] }), "a").unwrap();
assert_eq!(t.shape, vec![2, 2]);
assert_eq!(t.data, vec![7, 9, 2, 4]);
assert_eq!(t.len(), 4);
}
#[test]
fn parses_a_vector_a_scalar_and_a_3d_block() {
assert_eq!(
tensor(json!({ "a": [1, 2, 3] }), "a").unwrap().shape,
vec![3]
);
let s = tensor(json!({ "a": 5 }), "a").unwrap();
assert_eq!(s.shape, Vec::<i64>::new());
assert_eq!(s.data, vec![5]);
let c = tensor(json!({ "a": [[[1, 2]], [[3, 4]]] }), "a").unwrap();
assert_eq!(c.shape, vec![2, 1, 2]);
assert_eq!(c.data, vec![1, 2, 3, 4]);
}
#[test]
fn negatives_and_the_int64_extremes_round_trip() {
let t = tensor(json!({ "a": [[-1, i64::MIN, i64::MAX]] }), "a").unwrap();
assert_eq!(t.data, vec![-1, i64::MIN, i64::MAX]);
}
#[test]
fn rejects_a_ragged_row() {
let e = tensor(json!({ "a": [[3, 1], [2]] }), "a").unwrap_err();
let m = e.to_string();
assert!(m.contains("not rectangular"), "{m}");
assert!(m.contains("row of 1 at depth 1"), "{m}");
}
#[test]
fn rejects_a_scalar_where_a_row_belongs() {
let e = tensor(json!({ "a": [[3, 1], 2] }), "a").unwrap_err();
assert!(e.to_string().contains("scalar at depth 1"), "{e}");
}
#[test]
fn rejects_a_deeper_row() {
let e = tensor(json!({ "a": [[1, 2], [[3], [4]]] }), "a").unwrap_err();
assert!(e.to_string().contains("not rectangular"), "{e}");
}
#[test]
fn missing_output_names_the_ones_that_exist() {
let e = tensor(json!({ "a": [[1]], "t": [[1]] }), "zz").unwrap_err();
let m = e.to_string();
assert!(m.contains("no output named 'zz'"), "{m}");
assert!(m.contains("outputs are: a, t"), "{m}");
}
#[test]
fn decimals_are_refused_by_tensor_and_point_at_decimal() {
let e = tensor(json!({ "y": [[1.25]] }), "y").unwrap_err();
let m = e.to_string();
assert!(m.contains("not an integer"), "{m}");
assert!(m.contains("decimal(\"y\")"), "{m}");
}
#[test]
fn a_body_that_is_not_run_json_says_so() {
let e = tensor_from_run_json(&json!({ "t": [{ "a": 1 }] }), "t").unwrap_err();
assert!(e.to_string().contains("no 'outputs' object"), "{e}");
}
#[test]
fn json_member_returns_the_verbatim_span() {
let body = r#"{"outputs":{"y":[[3.00,5.00],[0.10,1.00]],"z":7},"notes":[]}"#;
let outputs = json_member(body, "outputs").unwrap();
assert_eq!(outputs, r#"{"y":[[3.00,5.00],[0.10,1.00]],"z":7}"#);
assert_eq!(
json_member(outputs, "y").unwrap(),
"[[3.00,5.00],[0.10,1.00]]"
);
assert_eq!(json_member(outputs, "z").unwrap(), "7");
assert_eq!(json_member(body, "notes").unwrap(), "[]");
assert_eq!(json_member(outputs, "nope"), None);
}
#[test]
fn json_member_is_not_confused_by_strings_or_whitespace() {
let body = r#"{ "a" : "}{,\"x\"" , "b" : { "a" : 1 } , "c" : -2.5e-3 }"#;
assert_eq!(json_member(body, "a").unwrap(), r#""}{,\"x\"""#);
assert_eq!(json_member(body, "b").unwrap(), r#"{ "a" : 1 }"#);
assert_eq!(json_member(body, "c").unwrap(), "-2.5e-3");
}
#[test]
fn json_member_declines_non_objects() {
assert_eq!(json_member("[1,2]", "a"), None);
assert_eq!(json_member("", "a"), None);
assert_eq!(json_member("{", "a"), None);
assert_eq!(json_member("not json", "a"), None);
}
}