DelSkayn / rquickjs

High level bindings to the quickjs javascript engine
MIT License
504 stars 63 forks source link

Constructor definition #152

Closed azam closed 1 year ago

azam commented 1 year ago

Hi!

I want to be able to define an Object.prototype.constructor for my custom object. I am trying to add a subset of ECMA402 to my project, which are not implemented as classes.

Is it possible to do it without classes? If yes, how would I change my minimal reproduction code below?

FYI replacing Name('John','Doe') with new Name('John','Doe') will throw an exception with message not a constructor.

use rquickjs::{Ctx, FromJs, IntoJs, Object, Value};

pub struct Name {
    first_name: String,
    last_name: String,
}

impl Name {
    pub fn new(first_name: String, last_name: String) -> Name {
        Name {
            first_name,
            last_name,
        }
    }

    pub fn full_name(self) -> String {
        format!("{},{}", self.last_name, self.first_name)
    }
}

impl<'js> IntoJs<'js> for Name {
    fn into_js(self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
        let obj = Object::new(ctx)?;
        obj.set("first_name", self.first_name)?;
        obj.set("last_name", self.last_name)?;
        Ok(obj.into())
    }
}

impl<'js> FromJs<'js> for Name {
    fn from_js(ctx: Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
        if let Some(obj) = value.as_object() {
            Ok(Name {
                first_name: obj.get("first_name").unwrap(),
                last_name: obj.get("last_name").unwrap(),
            })
        } else {
            Err(rquickjs::Error::Unknown)
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::*;
    use rquickjs::{Context, Func, Runtime};

    #[test]
    fn test() {
        let rt = Runtime::new().unwrap();
        let ctx = Context::full(&rt).unwrap();
        ctx.with(|ctx| {
            let globals = ctx.globals();
            // Should I use Constructor::new here?
            globals.set("Name", Func::new("Name", Name::new)).unwrap();
            // I want to use `new Name('John', 'Doe')` instead
            let actual: Name = ctx.eval("Name('John', 'Doe')").unwrap();
            assert_eq!("John".to_owned(), actual.first_name);
            assert_eq!("Doe".to_owned(), actual.last_name);
        });
    }
}

Great library btw, I like the custom allocator feature, which matched my use case perfectly.

azam commented 1 year ago

Found the bind attr macros. Will try out these first.

DelSkayn commented 1 year ago

If you want a function to be a constructor you should first turn it into a javascript function and then set it as a constructor using Function::set_constructor.

The bind macro does this for you by creating a constructor for class with Class::constructor

azam commented 1 year ago

@DelSkayn Thanks! I forgot I asked this. I eventually got to the Function::set_constructor.