use crate::metadata::{MetadataError, MetadataStore, MetadataValue};
use js_sys::Array;
use wasm_bindgen::prelude::*;
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
#[wasm_bindgen]
pub struct JsMetadataValue {
pub(crate) inner: MetadataValue,
}
#[wasm_bindgen]
impl JsMetadataValue {
#[wasm_bindgen(js_name = "fromString")]
#[must_use]
pub fn from_string(value: String) -> Self {
Self {
inner: MetadataValue::String(value),
}
}
#[wasm_bindgen(js_name = "fromInteger")]
#[allow(clippy::cast_possible_truncation)]
pub fn from_integer(value: f64) -> Result<Self, JsError> {
if !value.is_finite() {
return Err(JsError::new(
"Integer value must be finite (not NaN or Infinity)",
));
}
if value.fract() != 0.0 {
return Err(JsError::new(&format!(
"Value {value} is not an integer (has fractional part)"
)));
}
if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
return Err(JsError::new(&format!(
"Integer value {value} exceeds JavaScript safe integer range (±{MAX_SAFE_INTEGER})"
)));
}
Ok(Self {
inner: MetadataValue::Integer(value as i64),
})
}
#[wasm_bindgen(js_name = "fromFloat")]
#[must_use]
pub fn from_float(value: f64) -> Self {
Self {
inner: MetadataValue::Float(value),
}
}
#[wasm_bindgen(js_name = "fromBoolean")]
#[must_use]
pub fn from_boolean(value: bool) -> Self {
Self {
inner: MetadataValue::Boolean(value),
}
}
#[wasm_bindgen(js_name = "fromStringArray")]
#[allow(clippy::needless_pass_by_value)]
pub fn from_string_array(value: Array) -> Result<Self, JsError> {
let mut strings = Vec::with_capacity(value.length() as usize);
for i in 0..value.length() {
let item = value.get(i);
let s = item
.as_string()
.ok_or_else(|| JsError::new("Array elements must be strings"))?;
strings.push(s);
}
Ok(Self {
inner: MetadataValue::StringArray(strings),
})
}
#[wasm_bindgen(js_name = "getType")]
#[must_use]
pub fn get_type(&self) -> String {
self.inner.type_name().to_string()
}
#[wasm_bindgen(js_name = "isString")]
#[must_use]
pub fn is_string(&self) -> bool {
self.inner.is_string()
}
#[wasm_bindgen(js_name = "isInteger")]
#[must_use]
pub fn is_integer(&self) -> bool {
self.inner.is_integer()
}
#[wasm_bindgen(js_name = "isFloat")]
#[must_use]
pub fn is_float(&self) -> bool {
self.inner.is_float()
}
#[wasm_bindgen(js_name = "isBoolean")]
#[must_use]
pub fn is_boolean(&self) -> bool {
self.inner.is_boolean()
}
#[wasm_bindgen(js_name = "isStringArray")]
#[must_use]
pub fn is_string_array(&self) -> bool {
self.inner.is_string_array()
}
#[wasm_bindgen(js_name = "asString")]
#[must_use]
pub fn as_string(&self) -> Option<String> {
self.inner.as_string().map(String::from)
}
#[wasm_bindgen(js_name = "asInteger")]
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn as_integer(&self) -> Option<f64> {
self.inner.as_integer().map(|i| i as f64)
}
#[wasm_bindgen(js_name = "asFloat")]
#[must_use]
pub fn as_float(&self) -> Option<f64> {
self.inner.as_float()
}
#[wasm_bindgen(js_name = "asBoolean")]
#[must_use]
pub fn as_boolean(&self) -> Option<bool> {
self.inner.as_boolean()
}
#[wasm_bindgen(js_name = "asStringArray")]
#[must_use]
pub fn as_string_array(&self) -> JsValue {
match self.inner.as_string_array() {
Some(arr) => {
let js_array = Array::new();
for s in arr {
js_array.push(&JsValue::from_str(s));
}
js_array.into()
}
None => JsValue::UNDEFINED,
}
}
#[wasm_bindgen(js_name = "toJS")]
#[must_use]
pub fn to_js(&self) -> JsValue {
match &self.inner {
MetadataValue::String(s) => JsValue::from_str(s),
MetadataValue::Integer(i) => {
#[allow(clippy::cast_precision_loss)]
JsValue::from_f64(*i as f64)
}
MetadataValue::Float(f) => JsValue::from_f64(*f),
MetadataValue::Boolean(b) => JsValue::from_bool(*b),
MetadataValue::StringArray(arr) => {
let js_array = Array::new();
for s in arr {
js_array.push(&JsValue::from_str(s));
}
js_array.into()
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn metadata_error_to_js(e: MetadataError) -> JsError {
JsError::new(&e.to_string())
}
pub(crate) fn metadata_value_to_js(value: Option<&MetadataValue>) -> Option<JsMetadataValue> {
value.map(|v| JsMetadataValue { inner: v.clone() })
}
pub(crate) fn metadata_to_js_object(store: &MetadataStore, vector_id: u32) -> JsValue {
match store.get_all(vector_id) {
Some(metadata) => {
let obj = js_sys::Object::new();
for (key, value) in metadata {
let js_value = JsMetadataValue {
inner: value.clone(),
};
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str(key), &js_value.to_js());
}
obj.into()
}
None => JsValue::UNDEFINED,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_js_metadata_value_string() {
let value = JsMetadataValue::from_string("hello".to_string());
assert!(value.is_string());
assert_eq!(value.get_type(), "string");
assert_eq!(value.as_string(), Some("hello".to_string()));
}
#[test]
fn test_js_metadata_value_integer() {
let value = JsMetadataValue::from_integer(42.0).unwrap();
assert!(value.is_integer());
assert_eq!(value.get_type(), "integer");
assert_eq!(value.as_integer(), Some(42.0));
}
#[test]
fn test_from_integer_valid_range() {
assert!(JsMetadataValue::from_integer(0.0).is_ok());
assert!(JsMetadataValue::from_integer(-1.0).is_ok());
assert!(JsMetadataValue::from_integer(1_000_000.0).is_ok());
let max_safe = 9_007_199_254_740_991.0;
assert!(JsMetadataValue::from_integer(max_safe).is_ok());
assert!(JsMetadataValue::from_integer(-max_safe).is_ok());
}
#[test]
fn test_js_metadata_value_float() {
let value = JsMetadataValue::from_float(3.125);
assert!(value.is_float());
assert_eq!(value.get_type(), "float");
assert_eq!(value.as_float(), Some(3.125));
}
#[test]
fn test_js_metadata_value_boolean() {
let value = JsMetadataValue::from_boolean(true);
assert!(value.is_boolean());
assert_eq!(value.get_type(), "boolean");
assert_eq!(value.as_boolean(), Some(true));
}
}