use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use napi::bindgen_prelude::{Function, JsObjectValue, JsValue, Object, ObjectRef, Unknown};
use napi::{Env, Error, Result, Status};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Wrapper {
Date,
Timestamp,
Decimal,
Uuid,
}
impl Wrapper {
pub const ALL: &'static [Wrapper] = &[
Wrapper::Date,
Wrapper::Timestamp,
Wrapper::Decimal,
Wrapper::Uuid,
];
pub fn class_name(self) -> &'static str {
match self {
Wrapper::Date => "VcDate",
Wrapper::Timestamp => "VcTimestamp",
Wrapper::Decimal => "VcDecimal",
Wrapper::Uuid => "VcUuid",
}
}
}
const SOURCE: &str = include_str!("wrappers.js");
const VALUE: &str = "value";
const IS_PLAIN: &str = "isPlain";
static REALMS: LazyLock<Mutex<HashMap<usize, ObjectRef<false>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn registry_poisoned() -> Error {
Error::new(Status::GenericFailure, "wrapper class registry is poisoned")
}
pub struct Wrappers<'e> {
classes: Object<'e>,
}
impl<'e> Wrappers<'e> {
pub fn get(env: &'e Env) -> Result<Self> {
let key = env.raw() as usize;
let existing = REALMS
.lock()
.map_err(|_| registry_poisoned())?
.get(&key)
.map(|classes| classes.get_value(env))
.transpose()?;
let classes = match existing {
Some(classes) => classes,
None => {
let classes: Object<'e> = env.run_script(SOURCE)?;
let reference = classes.create_ref::<false>()?;
REALMS
.lock()
.map_err(|_| registry_poisoned())?
.insert(key, reference);
env.add_env_cleanup_hook(key, release_wrappers)?;
classes
}
};
Ok(Self { classes })
}
pub fn exports(env: &'e Env) -> Result<Object<'e>> {
let wrappers = Self::get(env)?;
let mut exports = Object::new(env)?;
for &wrapper in Wrapper::ALL {
exports.set_named_property(wrapper.class_name(), wrappers.constructor(wrapper)?)?;
}
Ok(exports)
}
fn constructor(&self, wrapper: Wrapper) -> Result<Function<'e, String, Unknown<'e>>> {
self.classes.get_named_property(wrapper.class_name())
}
pub fn classify(&self, value: &Unknown<'e>) -> Result<Option<Wrapper>> {
for &wrapper in Wrapper::ALL {
if value.instanceof(self.constructor(wrapper)?)? {
return Ok(Some(wrapper));
}
}
Ok(None)
}
pub fn is_plain(&self, value: Unknown<'e>) -> Result<bool> {
let is_plain: Function<'e, Unknown<'e>, bool> =
self.classes.get_named_property(IS_PLAIN)?;
is_plain.call(value).map_err(|error| {
let reason = error.reason.clone();
match Env::from_raw(self.classes.value().env).throw(error) {
Ok(()) => Error::new(Status::PendingException, reason),
Err(error) => error,
}
})
}
pub fn text(&self, instance: &Object<'e>) -> Result<Option<Unknown<'e>>> {
if !instance.has_own_property(VALUE)? {
return Ok(None);
}
let text = crate::js::get_property_unknown(instance, VALUE)?;
Ok((text.get_type()? == napi::ValueType::String).then_some(text))
}
pub fn construct(&self, wrapper: Wrapper, text: &str) -> Result<Unknown<'e>> {
self.constructor(wrapper)?.new_instance(text.to_owned())
}
}
#[cfg_attr(test, mutants::skip)]
fn release_wrappers(key: usize) {
let Ok(mut realms) = REALMS.lock() else {
return;
};
if let Some(reference) = realms.remove(&key) {
let env = Env::from_raw(key as napi::sys::napi_env);
let _ = reference.unref(&env);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_wrapper_has_the_class_name_javascript_sees() {
let names: Vec<&str> = Wrapper::ALL.iter().map(|w| w.class_name()).collect();
assert_eq!(names, ["VcDate", "VcTimestamp", "VcDecimal", "VcUuid"]);
for &wrapper in Wrapper::ALL {
assert_eq!(wrapper.class_name(), format!("Vc{wrapper:?}"));
}
}
}