use crate::value::JsValue;
use mozjs::jsapi::JSObject;
use mozjs::jsval::JSVal;
use mozjs::rooted;
#[derive(Debug, Clone)]
pub struct JSValue(pub(crate) JsValue);
impl JSValue {
pub const UNDEFINED: JSValue = JSValue(JsValue::Undefined);
pub const NULL: JSValue = JSValue(JsValue::Null);
pub const TRUE: JSValue = JSValue(JsValue::Bool(true));
pub const FALSE: JSValue = JSValue(JsValue::Bool(false));
pub const ZERO: JSValue = JSValue(JsValue::Number(0.0));
pub const ONE: JSValue = JSValue(JsValue::Number(1.0));
pub const NAN: JSValue = JSValue(JsValue::Number(f64::NAN));
pub const INFINITY: JSValue = JSValue(JsValue::Number(f64::INFINITY));
pub const NEG_INFINITY: JSValue = JSValue(JsValue::Number(f64::NEG_INFINITY));
}
impl JSValue {
#[inline]
pub fn is_undefined(&self) -> bool {
self.0.is_undefined()
}
#[inline]
pub fn is_null(&self) -> bool {
self.0.is_null()
}
#[inline]
pub fn is_boolean(&self) -> bool {
matches!(self.0, JsValue::Bool(_))
}
#[inline]
pub fn is_number(&self) -> bool {
self.0.is_number()
}
#[inline]
pub fn is_string(&self) -> bool {
self.0.is_string()
}
#[inline]
pub fn is_object(&self) -> bool {
self.0.is_object()
}
#[inline]
pub fn is_cell(&self) -> bool {
self.is_object()
}
pub fn is_true(&self) -> bool {
matches!(self.0, JsValue::Bool(true))
}
pub fn is_false(&self) -> bool {
matches!(self.0, JsValue::Bool(false))
}
pub fn is_falsy(&self) -> bool {
!self.to_boolean()
}
pub fn is_truthy(&self) -> bool {
self.to_boolean()
}
pub fn is_int32(&self) -> bool {
match self.0 {
JsValue::Number(n) => n == (n as i32) as f64 && n.abs() < i32::MAX as f64,
_ => false,
}
}
pub fn is_nan(&self) -> bool {
match self.0 {
JsValue::Number(n) => n.is_nan(),
_ => false,
}
}
}
impl JSValue {
#[inline]
pub fn as_boolean(&self) -> Option<bool> {
self.0.as_bool()
}
#[inline]
pub fn as_number(&self) -> Option<f64> {
self.0.as_number()
}
#[inline]
pub fn as_string(&self) -> Option<&str> {
self.0.as_string()
}
#[inline]
pub fn as_object(&self) -> Option<*mut JSObject> {
self.0.as_object()
}
#[inline]
pub fn as_cell(&self) -> Option<*mut JSObject> {
self.as_object()
}
pub fn to_boolean(&self) -> bool {
match &self.0 {
JsValue::Undefined | JsValue::Null => false,
JsValue::Bool(b) => *b,
JsValue::Number(n) => *n != 0.0 && !n.is_nan(),
JsValue::String(s) => !s.is_empty(),
JsValue::Object(_) => true,
}
}
pub fn to_number(&self) -> f64 {
match &self.0 {
JsValue::Number(n) => *n,
JsValue::Bool(true) => 1.0,
JsValue::Bool(false) => 0.0,
JsValue::Undefined => f64::NAN,
JsValue::Null => 0.0,
JsValue::String(s) => s.parse::<f64>().unwrap_or(f64::NAN),
JsValue::Object(_) => f64::NAN,
}
}
pub fn to_int32(&self) -> i32 {
self.to_number() as i32
}
pub fn to_display_string(&self) -> String {
self.0.to_display_string()
}
}
impl JSValue {
pub fn from_boolean(b: bool) -> Self {
JSValue(JsValue::Bool(b))
}
pub fn from_number(n: f64) -> Self {
JSValue(JsValue::Number(n))
}
pub fn from_string(s: String) -> Self {
JSValue(JsValue::String(s))
}
pub fn from_object(obj: *mut JSObject) -> Self {
JSValue(JsValue::Object(obj))
}
pub unsafe fn from_raw(cx: *mut mozjs::jsapi::JSContext, val: JSVal) -> Self {
JSValue(unsafe { crate::value::jsval_to_jsvalue(cx, val) })
}
pub fn from_inner(v: JsValue) -> Self {
JSValue(v)
}
pub fn into_inner(self) -> JsValue {
self.0
}
pub fn as_inner(&self) -> &JsValue {
&self.0
}
}
impl JSValue {
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn to_string(
&self,
cx: *mut mozjs::jsapi::JSContext,
) -> ::std::result::Result<String, crate::error::JsError> {
let js_val = self.0.to_jsval(cx);
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let val_root = js_val);
let js_str = unsafe { mozjs::rust::ToString(&mut wrapped_cx, val_root.handle().into()) };
if js_str.is_null() {
return Err(crate::error::JsError {
message: "ToString failed".into(),
filename: String::new(),
line: 0,
column: 0,
stack: None,
});
}
let nn = ::std::ptr::NonNull::new(js_str).unwrap();
Ok(unsafe { mozjs::conversions::unsafe_jsstr_to_string(cx, nn) })
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn to_object(
&self,
cx: *mut mozjs::jsapi::JSContext,
) -> ::std::result::Result<*mut JSObject, crate::error::JsError> {
match self.as_object() {
Some(obj) => Ok(obj),
None => {
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let js_val = self.0.to_jsval(cx);
rooted!(&in(wrapped_cx) let js_val_root = js_val);
let obj =
unsafe { mozjs::jsapi::ToObjectSlow(cx, js_val_root.handle().into(), false) };
if obj.is_null() {
Err(crate::error::JsError {
message: "ToObject failed".into(),
filename: String::new(),
line: 0,
column: 0,
stack: None,
})
} else {
Ok(obj)
}
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn to_number_sm(
&self,
cx: *mut mozjs::jsapi::JSContext,
) -> ::std::result::Result<f64, crate::error::JsError> {
let js_val = self.0.to_jsval(cx);
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let val_root = js_val);
unsafe { mozjs::rust::ToNumber(cx, val_root.handle().into()) }.map_err(|()| {
crate::error::JsError {
message: "ToNumber failed".into(),
filename: String::new(),
line: 0,
column: 0,
stack: None,
}
})
}
#[allow(unsafe_op_in_unsafe_fn)]
pub fn is_function(&self) -> bool {
match self.as_object() {
Some(obj) if !obj.is_null() => unsafe { mozjs::jsapi::JS_ObjectIsFunction(obj) },
_ => false,
}
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn is_array(&self, cx: *mut mozjs::jsapi::JSContext) -> bool {
match self.as_object() {
Some(obj) if !obj.is_null() => {
let mut is_array = false;
let cx_ref = &mut mozjs::context::JSContext::from_ptr(
::std::ptr::NonNull::new_unchecked(cx),
);
rooted!(&in(cx_ref) let obj_root = obj);
mozjs::jsapi::IsArrayObject1(cx, obj_root.handle().into(), &mut is_array);
is_array
}
_ => false,
}
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn is_error(&self, _cx: *mut mozjs::jsapi::JSContext) -> bool {
match self.as_object() {
Some(obj) if !obj.is_null() => {
let clasp = mozjs::rust::get_object_class(obj);
if clasp.is_null() {
return false;
}
let name_ptr = (*clasp).name;
if name_ptr.is_null() {
return false;
}
let name = ::std::ffi::CStr::from_ptr(name_ptr);
let name_str = name.to_string_lossy();
name_str.ends_with("Error")
}
_ => false,
}
}
pub fn as_function(&self) -> Option<*mut JSObject> {
if self.is_function() {
self.as_object()
} else {
None
}
}
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn get_property(&self, cx: *mut mozjs::jsapi::JSContext, name: &str) -> JSValue {
match self.to_object(cx) {
Ok(obj) => crate::value::get_property(cx, obj, name).into(),
Err(_) => JSValue::UNDEFINED,
}
}
}
impl From<JsValue> for JSValue {
fn from(v: JsValue) -> Self {
JSValue(v)
}
}
impl PartialEq for JSValue {
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(JsValue::Undefined, JsValue::Undefined) => true,
(JsValue::Null, JsValue::Null) => true,
(JsValue::Bool(a), JsValue::Bool(b)) => a == b,
(JsValue::Number(a), JsValue::Number(b)) => {
if a.is_nan() && b.is_nan() {
false } else {
a == b
}
}
(JsValue::String(a), JsValue::String(b)) => a == b,
(JsValue::Object(a), JsValue::Object(b)) => a == b,
_ => false,
}
}
}
impl Eq for JSValue {}
pub type RawJSValue = JSVal;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn undefined_is_falsy() {
assert!(JSValue::UNDEFINED.is_falsy());
assert!(!JSValue::UNDEFINED.is_truthy());
}
#[test]
fn null_is_falsy() {
assert!(JSValue::NULL.is_falsy());
}
#[test]
fn true_is_truthy() {
assert!(JSValue::TRUE.is_truthy());
}
#[test]
fn false_is_falsy() {
assert!(JSValue::FALSE.is_falsy());
}
#[test]
fn zero_is_falsy() {
assert!(JSValue::ZERO.is_falsy());
}
#[test]
fn one_is_truthy() {
assert!(JSValue::ONE.is_truthy());
}
#[test]
fn nan_is_falsy() {
assert!(JSValue::NAN.is_falsy());
}
#[test]
fn empty_string_is_falsy() {
assert!(JSValue::from_string(String::new()).is_falsy());
}
#[test]
fn nonempty_string_is_truthy() {
assert!(JSValue::from_string("hello".into()).is_truthy());
}
#[test]
fn nan_not_equal_nan() {
assert_ne!(JSValue::NAN, JSValue::NAN);
}
#[test]
fn to_number_conversions() {
assert_eq!(JSValue::TRUE.to_number(), 1.0);
assert_eq!(JSValue::FALSE.to_number(), 0.0);
assert!(JSValue::UNDEFINED.to_number().is_nan());
assert_eq!(JSValue::NULL.to_number(), 0.0);
}
#[test]
fn is_int32_true() {
assert!(JSValue::from_number(42.0).is_int32());
assert!(JSValue::from_number(0.0).is_int32());
}
#[test]
fn is_int32_false() {
assert!(!JSValue::from_number(3.14).is_int32());
assert!(!JSValue::from_number(f64::NAN).is_int32());
}
#[test]
fn from_inner_roundtrip() {
let inner = JsValue::Number(99.0);
let js_val = JSValue::from_inner(inner);
assert!(js_val.is_number());
assert_eq!(js_val.as_number(), Some(99.0));
let back = js_val.into_inner();
assert!(matches!(back, JsValue::Number(n) if n == 99.0));
}
}