use serde::Serialize;
use tensogram::{self as core, EncodeOptions};
use wasm_bindgen::prelude::*;
pub(crate) fn js_err(e: tensogram::TensogramError) -> JsValue {
use tensogram::TensogramError as E;
let js_err: JsValue = js_sys::Error::new(&e.to_string()).into();
match &e {
E::HashMismatch {
object_index,
expected,
actual,
} => {
attach_string_prop(&js_err, "name", "HashMismatchError");
if let Some(idx) = object_index {
attach_number_prop(&js_err, "objectIndex", *idx);
}
attach_string_prop(&js_err, "expected", expected);
attach_string_prop(&js_err, "actual", actual);
}
E::MissingHash { object_index } => {
attach_string_prop(&js_err, "name", "MissingHashError");
attach_number_prop(&js_err, "objectIndex", *object_index);
}
_ => {}
}
js_err
}
pub(crate) fn js_err_display<E: std::fmt::Display>(e: E) -> JsValue {
js_sys::Error::new(&e.to_string()).into()
}
fn attach_string_prop(err: &JsValue, key: &str, value: &str) {
let _ = js_sys::Reflect::set(err, &key.into(), &value.into());
}
fn attach_number_prop(err: &JsValue, key: &str, value: usize) {
let _ = js_sys::Reflect::set(err, &key.into(), &(value as f64).into());
}
pub(crate) fn build_encode_options(hash: Option<bool>) -> EncodeOptions {
build_encode_options_full(hash, None, None, None, None, None, None)
.expect("build_encode_options_full with no method names cannot error")
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_encode_options_full(
hash: Option<bool>,
allow_nan: Option<bool>,
allow_inf: Option<bool>,
nan_mask_method: Option<&str>,
pos_inf_mask_method: Option<&str>,
neg_inf_mask_method: Option<&str>,
small_mask_threshold_bytes: Option<usize>,
) -> Result<EncodeOptions, JsValue> {
use core::encode::MaskMethod;
let defaults = EncodeOptions::default();
let parse = |s: Option<&str>, d: MaskMethod| -> Result<MaskMethod, JsValue> {
match s {
Some(name) => MaskMethod::from_name(name).map_err(js_err_display),
None => Ok(d),
}
};
Ok(EncodeOptions {
hashing: hash.unwrap_or(true),
allow_nan: allow_nan.unwrap_or(false),
allow_inf: allow_inf.unwrap_or(false),
nan_mask_method: parse(nan_mask_method, defaults.nan_mask_method.clone())?,
pos_inf_mask_method: parse(pos_inf_mask_method, defaults.pos_inf_mask_method.clone())?,
neg_inf_mask_method: parse(neg_inf_mask_method, defaults.neg_inf_mask_method.clone())?,
small_mask_threshold_bytes: small_mask_threshold_bytes
.unwrap_or(defaults.small_mask_threshold_bytes),
..defaults
})
}
pub(crate) fn extract_descriptor_data_pairs(
objects_js: &js_sys::Array,
) -> Result<(Vec<core::DataObjectDescriptor>, Vec<Vec<u8>>), JsValue> {
let len = objects_js.length();
let mut descriptors = Vec::with_capacity(len as usize);
let mut data_vec = Vec::with_capacity(len as usize);
for i in 0..len {
let entry = objects_js.get(i);
let desc_val = js_sys::Reflect::get(&entry, &"descriptor".into())
.map_err(|_| JsValue::from(js_sys::Error::new("each object must have a 'descriptor' field")))?;
let data_val = js_sys::Reflect::get(&entry, &"data".into())
.map_err(|_| JsValue::from(js_sys::Error::new("each object must have a 'data' field")))?;
let desc: core::DataObjectDescriptor =
serde_wasm_bindgen::from_value(desc_val).map_err(js_err_display)?;
let data_bytes = typed_array_or_u8_to_bytes(&data_val)
.ok_or_else(|| JsValue::from(js_sys::Error::new("data must be a TypedArray, DataView, or Uint8Array")))?;
descriptors.push(desc);
data_vec.push(data_bytes);
}
Ok((descriptors, data_vec))
}
pub(crate) fn to_js<T: Serialize>(val: &T) -> Result<JsValue, JsValue> {
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
val.serialize(&serializer)
.map_err(|e| JsValue::from(js_sys::Error::new(&e.to_string())))
}
pub(crate) fn metadata_from_js(
metadata_js: &JsValue,
) -> Result<tensogram::GlobalMetadata, JsValue> {
use std::collections::BTreeMap;
use tensogram::RESERVED_KEY;
if !metadata_js.is_object() || metadata_js.is_null() {
return Err(JsValue::from(js_sys::Error::new(&format!(
"metadata must be a plain object, got {metadata_js:?}"
))));
}
let obj: &js_sys::Object = metadata_js.unchecked_ref();
if js_sys::Reflect::has(obj, &JsValue::from_str(RESERVED_KEY))
.map_err(|_| JsValue::from(js_sys::Error::new("internal: Reflect.has failed on metadata object")))?
{
return Err(JsValue::from(js_sys::Error::new(&format!(
"'{RESERVED_KEY}' must not be set by client code — the encoder populates it"
))));
}
let base: Vec<BTreeMap<String, ciborium::Value>> =
match js_sys::Reflect::get(obj, &JsValue::from_str("base"))
.map_err(|_| JsValue::from(js_sys::Error::new("internal: Reflect.get('base') failed")))?
{
v if v.is_undefined() => Vec::new(),
v => serde_wasm_bindgen::from_value(v).map_err(js_err_display)?,
};
for (i, entry) in base.iter().enumerate() {
if entry.contains_key(RESERVED_KEY) {
return Err(JsValue::from(js_sys::Error::new(&format!(
"base[{i}] must not contain '{RESERVED_KEY}' — the encoder populates it"
))));
}
}
let mut extra: BTreeMap<String, ciborium::Value> =
match js_sys::Reflect::get(obj, &JsValue::from_str("_extra_"))
.map_err(|_| JsValue::from(js_sys::Error::new("internal: Reflect.get('_extra_') failed")))?
{
v if v.is_undefined() => BTreeMap::new(),
v => serde_wasm_bindgen::from_value(v).map_err(js_err_display)?,
};
const KNOWN: &[&str] = &["base", "_extra_", RESERVED_KEY];
let own_keys = js_sys::Object::keys(obj);
for i in 0..own_keys.length() {
let key_val = own_keys.get(i);
let key = match key_val.as_string() {
Some(s) => s,
None => continue, };
if KNOWN.contains(&key.as_str()) {
continue;
}
if extra.contains_key(&key) {
continue; }
let value = js_sys::Reflect::get(obj, &key_val)
.map_err(|_| JsValue::from(js_sys::Error::new("internal: Reflect.get for free-form key failed")))?;
let cbor: ciborium::Value = serde_wasm_bindgen::from_value(value).map_err(js_err_display)?;
extra.insert(key, cbor);
}
Ok(tensogram::GlobalMetadata {
base,
reserved: BTreeMap::new(),
extra,
})
}
pub(crate) fn metadata_to_js(meta: &tensogram::GlobalMetadata) -> Result<JsValue, JsValue> {
let obj = to_js(meta)?;
let reflect = js_sys::Reflect::set(
&obj,
&JsValue::from_str("version"),
&JsValue::from(tensogram::WIRE_VERSION),
);
if reflect.is_err() {
return Err(JsValue::from(js_sys::Error::new(
"internal: failed to set synthetic `version` on metadata JS object",
)));
}
Ok(obj)
}
pub(crate) fn view_as_f32(data: &[u8]) -> Result<js_sys::Float32Array, JsValue> {
if !data.len().is_multiple_of(4) {
return Err(JsValue::from(js_sys::Error::new(&format!(
"data length {} is not a multiple of 4 (Float32)",
data.len()
))));
}
if data.is_empty() {
return Ok(js_sys::Float32Array::new_with_length(0));
}
let byte_offset = data.as_ptr() as u32;
let length = (data.len() / 4) as u32;
let memory = wasm_bindgen::memory().unchecked_into::<js_sys::WebAssembly::Memory>();
Ok(js_sys::Float32Array::new_with_byte_offset_and_length(
&memory.buffer(),
byte_offset,
length,
))
}
pub(crate) fn view_as_f64(data: &[u8]) -> Result<js_sys::Float64Array, JsValue> {
if !data.len().is_multiple_of(8) {
return Err(JsValue::from(js_sys::Error::new(&format!(
"data length {} is not a multiple of 8 (Float64)",
data.len()
))));
}
if data.is_empty() {
return Ok(js_sys::Float64Array::new_with_length(0));
}
let byte_offset = data.as_ptr() as u32;
let length = (data.len() / 8) as u32;
let memory = wasm_bindgen::memory().unchecked_into::<js_sys::WebAssembly::Memory>();
Ok(js_sys::Float64Array::new_with_byte_offset_and_length(
&memory.buffer(),
byte_offset,
length,
))
}
pub(crate) fn view_as_i32(data: &[u8]) -> Result<js_sys::Int32Array, JsValue> {
if !data.len().is_multiple_of(4) {
return Err(JsValue::from(js_sys::Error::new(&format!(
"data length {} is not a multiple of 4 (Int32)",
data.len()
))));
}
if data.is_empty() {
return Ok(js_sys::Int32Array::new_with_length(0));
}
let byte_offset = data.as_ptr() as u32;
let length = (data.len() / 4) as u32;
let memory = wasm_bindgen::memory().unchecked_into::<js_sys::WebAssembly::Memory>();
Ok(js_sys::Int32Array::new_with_byte_offset_and_length(
&memory.buffer(),
byte_offset,
length,
))
}
pub(crate) fn view_as_u8(data: &[u8]) -> js_sys::Uint8Array {
if data.is_empty() {
return js_sys::Uint8Array::new_with_length(0);
}
unsafe { js_sys::Uint8Array::view(data) }
}
pub(crate) fn copy_as_f32(data: &[u8]) -> Result<js_sys::Float32Array, JsValue> {
let view = view_as_f32(data)?;
Ok(js_sys::Float32Array::new(&view))
}
pub(crate) fn typed_array_or_u8_to_bytes(val: &JsValue) -> Option<Vec<u8>> {
if let Some(arr) = val.dyn_ref::<js_sys::Uint8Array>() {
return Some(arr.to_vec());
}
if let Some(dv) = val.dyn_ref::<js_sys::DataView>() {
let u8 = js_sys::Uint8Array::new_with_byte_offset_and_length(
&dv.buffer(),
dv.byte_offset() as u32,
dv.byte_length() as u32,
);
return Some(u8.to_vec());
}
macro_rules! try_typed {
($ty:ty) => {
if let Some(arr) = val.dyn_ref::<$ty>() {
let u8 = js_sys::Uint8Array::new_with_byte_offset_and_length(
&arr.buffer(),
arr.byte_offset(),
arr.byte_length(),
);
return Some(u8.to_vec());
}
};
}
try_typed!(js_sys::Int8Array);
try_typed!(js_sys::Uint8ClampedArray);
try_typed!(js_sys::Int16Array);
try_typed!(js_sys::Uint16Array);
try_typed!(js_sys::Int32Array);
try_typed!(js_sys::Uint32Array);
try_typed!(js_sys::Float32Array);
try_typed!(js_sys::Float64Array);
try_typed!(js_sys::BigInt64Array);
try_typed!(js_sys::BigUint64Array);
None
}