use core::fmt;
use std::fmt::Debug;
use crate::utils::{
object::define_subclass,
option::Undefined,
primordials::{BasePrimordials, Primordial},
};
use rquickjs::{
atom::PredefinedAtom,
class::{
impl_::{CloneTrait, CloneWrapper},
JsClass, Trace,
},
function::{Constructor, Opt},
object::{Accessor, Property},
prelude::{Func, This},
qjs, Class, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, Value,
};
#[derive(JsLifetime)]
struct ExceptionPrimordials<'js> {
constructor_dom_exception: Constructor<'js>,
constructor_quota_exceeded_error: Constructor<'js>,
}
impl<'js> Primordial<'js> for ExceptionPrimordials<'js> {
fn new(ctx: &Ctx<'js>) -> Result<Self> {
let globals = ctx.globals();
Ok(Self {
constructor_dom_exception: globals.get(DOMException::NAME)?,
constructor_quota_exceeded_error: globals.get("QuotaExceededError")?,
})
}
}
#[derive(Trace, JsLifetime, Debug)]
pub struct DOMException {
name: String,
message: String,
stack: String,
code: u8,
}
impl fmt::Display for DOMException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DOMException")
.field("name", &self.name())
.field("message", &self.message())
.field("stack", &self.stack)
.finish()
}
}
fn add_constants(obj: &Object<'_>) -> Result<()> {
const CONSTANTS: [(&str, u8); 25] = [
("INDEX_SIZE_ERR", 1),
("DOMSTRING_SIZE_ERR", 2),
("HIERARCHY_REQUEST_ERR", 3),
("WRONG_DOCUMENT_ERR", 4),
("INVALID_CHARACTER_ERR", 5),
("NO_DATA_ALLOWED_ERR", 6),
("NO_MODIFICATION_ALLOWED_ERR", 7),
("NOT_FOUND_ERR", 8),
("NOT_SUPPORTED_ERR", 9),
("INUSE_ATTRIBUTE_ERR", 10),
("INVALID_STATE_ERR", 11),
("SYNTAX_ERR", 12),
("INVALID_MODIFICATION_ERR", 13),
("NAMESPACE_ERR", 14),
("INVALID_ACCESS_ERR", 15),
("VALIDATION_ERR", 16),
("TYPE_MISMATCH_ERR", 17),
("SECURITY_ERR", 18),
("NETWORK_ERR", 19),
("ABORT_ERR", 20),
("URL_MISMATCH_ERR", 21),
("QUOTA_EXCEEDED_ERR", 22),
("TIMEOUT_ERR", 23),
("INVALID_NODE_TYPE_ERR", 24),
("DATA_CLONE_ERR", 25),
];
for (key, value) in CONSTANTS {
obj.prop(key, Property::from(value).enumerable())?;
}
Ok(())
}
impl<'js> JsClass<'js> for DOMException {
const NAME: &'static str = "DOMException";
type Mutable = rquickjs::class::Writable;
fn prototype(ctx: &Ctx<'js>) -> rquickjs::Result<Option<Object<'js>>> {
use rquickjs::class::impl_::{MethodImpl, MethodImplementor};
let proto = Object::new(ctx.clone())?;
let implementor = MethodImpl::<Self>::new();
implementor.implement(&proto)?;
add_constants(&proto)?;
Ok(Some(proto))
}
fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
use rquickjs::class::impl_::{ConstructorCreate, ConstructorCreator};
let implementor = ConstructorCreate::<Self>::new();
let constructor = implementor
.create_constructor(ctx)?
.expect("DOMException must have a constructor");
add_constants(&constructor)?;
Ok(Some(constructor))
}
}
impl<'js> IntoJs<'js> for DOMException {
fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> Result<Value<'js>> {
let cls = Class::<Self>::instance(ctx.clone(), self)?;
IntoJs::into_js(cls, ctx)
}
}
impl<'js> FromJs<'js> for DOMException
where
for<'a> CloneWrapper<'a, Self>: CloneTrait<Self>,
{
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
let value = Class::<Self>::from_js(ctx, value)?;
let borrow = value.try_borrow()?;
Ok(CloneWrapper(&*borrow).wrap_clone())
}
}
#[rquickjs::methods]
impl DOMException {
#[qjs(constructor)]
pub fn new<'js>(
ctx: Ctx<'js>,
this: This<Value<'js>>,
message: Opt<Undefined<Coerced<String>>>,
name: Opt<Undefined<Coerced<String>>>,
) -> Result<Self> {
if this.0.as_function().is_none() {
return Err(Exception::throw_type(
&ctx,
"Cannot call the DOMException constructor without 'new'",
));
}
let message = match message.0 {
Some(Undefined(Some(message))) => message.0,
_ => String::new(),
};
let name = match name.0 {
Some(Undefined(Some(message))) => DOMExceptionName::from(message.0),
_ => DOMExceptionName::Error,
};
Self::new_with_name(&ctx, name, message)
}
#[qjs(skip)]
pub fn new_with_name(ctx: &Ctx<'_>, name: DOMExceptionName, message: String) -> Result<Self> {
let primordials = BasePrimordials::get(ctx)?;
let new: Object = primordials
.constructor_error
.construct((message.clone(),))?;
Ok(Self {
name: name.as_str().to_string(),
code: name.code(),
message,
stack: new.get::<_, String>(PredefinedAtom::Stack)?,
})
}
#[qjs(get, enumerable, configurable)]
fn message(&self) -> &str {
self.message.as_str()
}
#[qjs(get, enumerable, configurable)]
pub fn name(&self) -> &str {
self.name.as_str()
}
#[qjs(get, enumerable, configurable)]
pub fn code(&self) -> u8 {
self.code
}
#[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
stringify!(DOMException)
}
}
impl<'js> DOMException {
fn create(
ctx: &Ctx<'js>,
name: DOMExceptionName,
message: impl Into<String>,
) -> Result<Value<'js>> {
let primordials = ExceptionPrimordials::get(ctx)?;
let ctor = match name {
DOMExceptionName::QuotaExceededError => &primordials.constructor_quota_exceeded_error,
_ => &primordials.constructor_dom_exception,
};
ctor.construct((message.into(), name.as_str()))
}
fn throw_value(ctx: &Ctx<'js>, value: Value<'js>) -> Error {
unsafe {
let dup = qjs::JS_DupValue(ctx.as_raw().as_ptr(), value.as_raw());
qjs::JS_Throw(ctx.as_raw().as_ptr(), dup);
}
Error::Exception
}
fn create_error(ctx: &Ctx<'js>, name: DOMExceptionName, message: impl Into<String>) -> Error {
let value = Self::create(ctx, name, message).expect("failed to create DOMException");
Self::throw_value(ctx, value)
}
pub fn not_supported_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::NotSupportedError, message)
}
pub fn type_mismatch_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::TypeMismatchError, message)
}
pub fn operation_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::OperationError, message)
}
pub fn quota_exceeded_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::QuotaExceededError, message)
}
pub fn data_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::DataError, message)
}
pub fn invalid_access_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::InvalidAccessError, message)
}
pub fn syntax_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
Self::create_error(ctx, DOMExceptionName::SyntaxError, message)
}
fn define_quota_exceeded_error(ctx: &Ctx<'js>) -> Result<()> {
let dom_exception: Constructor = ctx.globals().get(Self::NAME)?;
let quota_exceeded_error = define_subclass(
ctx,
"QuotaExceededError",
&dom_exception,
|ctx, message: Opt<Undefined<Coerced<String>>>| {
let message = match message.0 {
Some(Undefined(Some(m))) => m.0,
_ => String::new(),
};
Self::new_with_name(&ctx, DOMExceptionName::QuotaExceededError, message)
},
)?;
let null = Value::new_null(ctx.clone());
let proto: Object = quota_exceeded_error.get(PredefinedAtom::Prototype)?;
proto.prop(
"requested",
Property::from(null.clone()).enumerable().configurable(),
)?;
proto.prop("quota", Property::from(null).enumerable().configurable())?;
ctx.globals().prop(
"QuotaExceededError",
Property::from(quota_exceeded_error)
.writable()
.configurable(),
)
}
}
macro_rules! create_dom_exception {
($name:ident, $($variant:ident),+ $(,)?) => {
#[derive(Debug)]
pub enum $name {
$(
$variant,
)+
Other(String),
}
impl $name {
pub fn as_str(&self) -> &str {
match self {
$(
Self::$variant => stringify!($variant),
)+
Self::Other(value) => value,
}
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
match value.as_str() {
$(
stringify!($variant) => Self::$variant,
)+
_ => Self::Other(value),
}
}
}
};
}
create_dom_exception!(
DOMExceptionName,
IndexSizeError,
HierarchyRequestError,
WrongDocumentError,
InvalidCharacterError,
NoModificationAllowedError,
NotFoundError,
NotSupportedError,
InUseAttributeError,
InvalidStateError,
SyntaxError,
InvalidModificationError,
NamespaceError,
InvalidAccessError,
TypeMismatchError,
SecurityError,
NetworkError,
AbortError,
URLMismatchError,
QuotaExceededError,
TimeoutError,
InvalidNodeTypeError,
DataCloneError,
EncodingError,
NotReadableError,
UnknownError,
ConstraintError,
DataError,
TransactionInactiveError,
ReadOnlyError,
VersionError,
OperationError,
NotAllowedError,
Error,
);
impl DOMExceptionName {
fn code(&self) -> u8 {
match self {
DOMExceptionName::IndexSizeError => 1,
DOMExceptionName::HierarchyRequestError => 3,
DOMExceptionName::WrongDocumentError => 4,
DOMExceptionName::InvalidCharacterError => 5,
DOMExceptionName::NoModificationAllowedError => 7,
DOMExceptionName::NotFoundError => 8,
DOMExceptionName::NotSupportedError => 9,
DOMExceptionName::InUseAttributeError => 10,
DOMExceptionName::InvalidStateError => 11,
DOMExceptionName::SyntaxError => 12,
DOMExceptionName::InvalidModificationError => 13,
DOMExceptionName::NamespaceError => 14,
DOMExceptionName::InvalidAccessError => 15,
DOMExceptionName::TypeMismatchError => 17,
DOMExceptionName::SecurityError => 18,
DOMExceptionName::NetworkError => 19,
DOMExceptionName::AbortError => 20,
DOMExceptionName::URLMismatchError => 21,
DOMExceptionName::QuotaExceededError => 22,
DOMExceptionName::TimeoutError => 23,
DOMExceptionName::InvalidNodeTypeError => 24,
DOMExceptionName::DataCloneError => 25,
_ => 0,
}
}
}
pub fn init(ctx: &Ctx<'_>) -> Result<()> {
let globals = ctx.globals();
BasePrimordials::init(ctx)?;
if let Some(constructor) = Class::<DOMException>::create_constructor(ctx)? {
globals.prop(
DOMException::NAME,
Property::from(constructor).writable().configurable(),
)?;
}
let dom_ex_proto = Class::<DOMException>::prototype(ctx)?.unwrap();
dom_ex_proto.set_prototype(Some(&BasePrimordials::get(ctx)?.prototype_error))?;
DOMException::define_quota_exceeded_error(ctx)?;
ExceptionPrimordials::init(ctx)?;
BasePrimordials::get(ctx)?
.constructor_error
.set("isError", Func::from(is_error))?;
define_error_stack_accessor(ctx)?;
Ok(())
}
fn define_error_stack_accessor<'js>(ctx: &Ctx<'js>) -> Result<()> {
let dom_ex_proto = Class::<DOMException>::prototype(ctx)?
.expect("DOMException prototype is defined by the call above");
dom_ex_proto.prop(
PredefinedAtom::Stack,
Accessor::new(
|this: This<Value<'js>>| -> Result<String> {
let stack = Class::<DOMException>::from_value(&this.0)
.ok()
.map(|cls| cls.borrow().stack.clone());
Ok(stack.unwrap_or_default())
},
|ctx: Ctx<'js>, this: This<Value<'js>>, value: Value<'js>| -> Result<()> {
let Some(obj) = this.0.as_object() else {
return Ok(());
};
if let Ok(Some(home)) = Class::<DOMException>::prototype(&ctx) {
if *obj == home {
return Ok(());
}
}
obj.prop(
PredefinedAtom::Stack,
Property::from(value).writable().enumerable().configurable(),
)
},
)
.configurable(),
)
}
fn is_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result<bool> {
if value.is_error() {
return Ok(true);
}
let Some(obj) = value.as_object() else {
return Ok(false);
};
let dom_exception: Value = ctx.globals().get(DOMException::NAME)?;
Ok(obj.is_instance_of(&dom_exception))
}