diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec24cb5..325e93e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: features: # No features at all - "" - # lua54+vendored base, then all combinations of: send, async, derive + # lua54+vendored base, then all combinations of: send, async, macros - lua54,vendored - lua54,vendored,send - lua54,vendored,async @@ -31,12 +31,12 @@ jobs: - lua54,vendored,send,macros - lua54,vendored,async,macros - lua54,vendored,send,async,macros - # Full feature set including serde and macros - - lua54,vendored,send,async,serde,macros + # Full feature set including macros + - lua54,vendored,send,async,macros # Luau feature combinations - luau,vendored - luau,vendored,macros,userdata-wrappers - - luau,vendored,send,async,serde,macros + - luau,vendored,send,async,macros steps: - uses: actions/checkout@v6 diff --git a/Cargo.lock b/Cargo.lock index 2f6d979..e132442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -339,6 +345,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab" dependencies = [ + "anyhow", "bstr", "either", "erased-serde", @@ -360,8 +367,9 @@ version = "11.6.1" dependencies = [ "mlua", "mlua-extras-derive", + "ryu", "serde", - "strum", + "strum 0.27.2", "tempfile", "tokio", ] @@ -375,6 +383,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", + "strum 0.28.0", "syn 2.0.87", "venial", ] @@ -617,6 +626,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -702,7 +717,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -717,6 +741,18 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 6894dbc..5f26c6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,23 @@ features = ["mlua", "lua54", "send", "async", "macros", "vendored"] [features] mlua = ["dep:mlua"] -lua54 = ["mlua", "mlua/lua54"] -lua53 = ["mlua", "mlua/lua53"] -lua52 = ["mlua", "mlua/lua52"] -lua51 = ["mlua", "mlua/lua51"] -luajit = ["mlua", "mlua/luajit"] -luau = ["mlua", "mlua/luau"] +lua55 = ["mlua/lua55", "mlua"] +lua54 = ["mlua/lua54", "mlua"] +lua53 = ["mlua/lua53", "mlua"] +lua52 = ["mlua/lua52", "mlua"] +lua51 = ["mlua/lua51", "mlua"] +luajit = ["mlua/luajit", "mlua"] +luajit52 = ["mlua/luajit52", "mlua"] +luau = ["mlua/luau", "mlua"] +luau-jit = ["mlua/luau", "mlua"] +luau-vector4 = ["mlua/luau-vector4", "mlua"] vendored = ["mlua/vendored"] -serde = ["mlua/serde"] -macros = ["mlua/macros", "dep:mlua-extras-derive"] module = ["mlua/module"] -send = ["mlua/send"] async = ["mlua/async"] +send = ["mlua/send"] +error-send = ["mlua/error-send"] +macros = ["mlua/macros", "dep:mlua-extras-derive"] +anyhow = ["mlua/anyhow"] userdata-wrappers = ["mlua/userdata-wrappers"] [dev-dependencies] @@ -37,8 +42,11 @@ tokio = { version = "1", features = ["macros", "rt"] } [dependencies] mlua-extras-derive = { path = "./mlua_extras_derive", version = "11.6.0", optional = true } -mlua = { version = "0.11.6", optional = true, default-features = false } +mlua = { version = "0.11.6", optional = true, default-features = false, features = ["serde"] } + +serde = { version = "1.0.228" } strum = { version = "0.27.2", features = ["derive"], default-features = false } +ryu = "1.0.23" [[example]] name = "macros" diff --git a/README.md b/README.md index e88fbec..d0ad862 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,154 @@ function greet(name) end function printColor(param0) end ``` +## Macros + +There are helper macros that make writing lua integrations simplier and less manual. There +are variants that support recording type information, and variants that just focus on making +the creation of custom userdata types simple. + +```rust +use std::path::PathBuf; +use mlua_extras::{ + TypedUserData, + typed::generator::{ + Definition, DefinitionFileGenerator, Definitions, LuauDefinitionFileGenerator, + }, + typed_user_data_impl, +}; + +/// Simple Counter +#[derive(Clone, TypedUserData)] +struct Counter { value: i64 } + +#[typed_user_data_impl] +impl Counter { + /// The default count + const COUNT: usize = 10; + + /// Max count value + #[field] + fn max() -> i64 { + i64::MAX + } + + /// Min count value + #[field(rename = "MIN")] + fn min() -> i64 { + 0 + } + + /// Direction of the counter + #[getter("direction")] + fn get_direction(&self) -> String { + "up".into() + } + + #[setter("direction")] + fn set_direction(&mut self, dir: String) { + println!("Direction: {dir}"); + } + + /// Get the current counter value + #[method] + fn get(&self) -> i64 { self.value } + + /// Increment the counter + #[method] + fn increment(&mut self) { self.value += 1 } + + /// Create a new table + #[method] + fn create_table(&self, lua: &mlua::Lua) -> mlua::Result { + lua.create_table() + } + + /// String representation of the counter + #[metamethod(ToString)] + fn to_string(&self) -> String { format!("Counter({})", self.value) } + + // Requires the `async` feature + // Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async` + + /// Fetch the global counter online + #[method] + async fn fetch(&self, lua: mlua::Lua, url: String) -> mlua::Result { + _ = lua; + Ok(format!("fetched: {url}")) + } +} + +fn main() -> mlua::Result<()> { + let definitions: Definitions = Definitions::start() + .define("macros", Definition::start().register::("Counter")) + .finish(); + + let types_path = PathBuf::from("examples/types"); + if !types_path.exists() { + std::fs::create_dir_all(&types_path).unwrap(); + } + + let dfg = DefinitionFileGenerator::new(definitions.clone()); + for (name, writer) in dfg.iter() { + println!("==== Generated \x1b[1;33mexample/types/{name}\x1b[0m ===="); + writer.write_file(types_path.join(name)).unwrap(); + } + + Ok(()) +} +``` + +Results in the lua type definition + +```lua +--- @meta + +--- Simple Counter +--- @class Counter +--- Direction of the counter +--- @field direction string +--- @field value integer +local _CLASS_Counter_ = { + --- The default count + COUNT = 10, + --- Min count value + MIN = 0, + --- Max count value + max = 9223372036854775807, + --- Create a new table + --- @param self Counter + --- @return table + create_table = function(self) end, + --- Fetch the global counter online + --- @param self Counter + --- @param url string + --- @return string + fetch = function(self, url) end, + --- Get the current counter value + --- @param self Counter + --- @return integer + get = function(self) end, + --- Increment the counter + --- @param self Counter + increment = function(self) end, + __metatable = { + --- @param param1 userdata + --- @param param2 any + --- @return any + __index = function(param1, param2) end, + --- @param param1 userdata + --- @param param2 any + --- @param param3 any + --- @return any | nil + __newindex = function(param1, param2, param3) end, + --- String representation of the counter + --- @param self Counter + --- @return string + __tostring = function(self) end, + } +} +``` + ## Testing To run all the tests in one shot, use `cargo test --features luau,vendored,send,async,serialize,derive` diff --git a/examples/init.lua b/examples/init.lua index f1a1813..e28e1fc 100644 --- a/examples/init.lua +++ b/examples/init.lua @@ -1 +1,14 @@ +--- @return Custom +function getCustom() return {} end + +--- @type Custom +local c = getCustom() + +if c._variant == "B" then + ---@cast c CustomB + print(c.COUNT) +end + +print(c.COUNT) + print("Hello world!") diff --git a/examples/init.luau b/examples/init.luau new file mode 100644 index 0000000..2096175 --- /dev/null +++ b/examples/init.luau @@ -0,0 +1 @@ +local _c: Custom = { {} :: any } \ No newline at end of file diff --git a/examples/macros.rs b/examples/macros.rs index 6e3ed8c..b3ec731 100644 --- a/examples/macros.rs +++ b/examples/macros.rs @@ -1,89 +1,99 @@ -use mlua::{IntoLua, FromLua, Lua, StdLib}; -use mlua_extras::{UserData, Typed, user_data_impl}; +use std::path::PathBuf; -#[derive(Clone, UserData)] -struct Data { - name: String -} +use mlua_extras::{ + TypedUserData, + typed::generator::{ + Definition, DefinitionFileGenerator, Definitions, LuauDefinitionFileGenerator, + }, + typed_user_data_impl, +}; -#[user_data_impl] -impl Data { - #[method] - fn get_data(&self) -> mlua::Result { - Ok(self.name.clone()) +/// Simple Counter +#[derive(Clone, TypedUserData)] +struct Counter { value: i64 } + +#[typed_user_data_impl] +impl Counter { + /// The default count + const COUNT: usize = 10; + + /// Max count value + #[field] + fn max() -> i64 { + i64::MAX } - /// This method is called last. - /// - /// use `#[field(skip)]` for fields that are assigned to the index - /// to allow for them to overridden in this impl - #[metamethod(Index)] - fn index(&self, lua: &Lua, idx: isize) -> mlua::Result { - match idx { - -1 => "TESTING".into_lua(lua), - 1 => self.name.clone().into_lua(lua), - _ => Ok(mlua::Value::Nil) - } + /// Min count value + #[field(rename = "MIN")] + fn min() -> i64 { + 0 } - - /// This method is called last. - /// - /// use `#[field(skip)]` for fields that are assigned to the index - /// to allow for them to overridden in this impl - #[metamethod(NewIndex)] - fn new_index(&mut self, lua: &Lua, idx: isize, value: mlua::Value) -> mlua::Result<()> { - match idx { - 1 => self.name = ::from_lua(value, lua)?, - // It is recommended to return some sort of error from this implementation. - // - // This enforces strict indexing into userdata types. - _ => return Err(mlua::Error::runtime(format!("invalid index '{idx}'"))) - } - Ok(()) + + /// Direction of the counter + #[getter("direction")] + fn get_direction(&self) -> String { + "up".into() } -} -#[derive(Clone, UserData)] -enum Kind { - A, - B(String), - C { - name: String, - age: u8, - }, - D(u32), -} + #[setter("direction")] + fn set_direction(&mut self, dir: String) { + println!("Direction: {dir}"); + } -#[user_data_impl] -impl Kind { + /// Get the current counter value #[method] - fn message(&self) -> String { - match self { - Self::A => "Hello, world!".into(), - Self::B(msg) => msg.clone(), - Self::C{ name, age } => format!("{name} age {age}"), - Self::D(count) => count.to_string() - } + fn get(&self) -> i64 { self.value } + + /// Increment the counter + #[method] + fn increment(&mut self) { self.value += 1 } + + /// Create a new table + #[method] + fn create_table(&self, lua: &mlua::Lua) -> mlua::Result { + lua.create_table() + } + + /// String representation of the counter + #[metamethod(ToString)] + fn to_string(&self) -> String { format!("Counter({})", self.value) } + + // Requires the `async` feature + // Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async` + + /// Fetch the global counter online + #[method] + async fn fetch(&self, lua: mlua::Lua, url: String) -> mlua::Result { + _ = lua; + Ok(format!("fetched: {url}")) } } fn main() -> mlua::Result<()> { - let lua = unsafe { Lua::unsafe_new_with(StdLib::ALL, Default::default()) }; - - lua.globals().set("data", Data { name: "MluaExtras".into() })?; - lua.globals().set("kind", Kind::A)?; - - lua.load(" - print('Index [1]:', data[1]) - data[1] = 'HelloWorld' - print('Set data[1] to \\'HelloWorld\\'') - print('Get Data:', data:get_data()) - print('Index [-1]:', data[-1]) - print('Kind:', kind._variant, kind:message()) - - local ok, value = pcall(function() return kind[1] end) - print('Kind [1]: OK', ok, value) - ").exec()?; + let definitions: Definitions = Definitions::start() + .define( + "macros", + Definition::start() + .register::("Counter") + ) + .finish(); + + let types_path = PathBuf::from("examples/types"); + if !types_path.exists() { + std::fs::create_dir_all(&types_path).unwrap(); + } + + let dfg = DefinitionFileGenerator::new(definitions.clone()); + for (name, writer) in dfg.iter() { + println!("==== Generated \x1b[1;33mexample/types/{name}\x1b[0m ===="); + writer.write_file(types_path.join(name)).unwrap(); + } + + let luau_gen = LuauDefinitionFileGenerator::new(definitions); + for (name, writer) in luau_gen.iter() { + println!("==== Generated \x1b[1;33mexample/types/{name}\x1b[0m ===="); + writer.write_file(types_path.join(name)).unwrap(); + } Ok(()) } diff --git a/examples/types/macros.d.lua b/examples/types/macros.d.lua new file mode 100644 index 0000000..18715b8 --- /dev/null +++ b/examples/types/macros.d.lua @@ -0,0 +1,47 @@ +--- @meta + +--- Simple Counter +--- @class Counter +--- Direction of the counter +--- @field direction string +--- @field value integer +local _CLASS_Counter_ = { + --- The default count + COUNT = 10, + --- Min count value + MIN = 0, + --- Max count value + max = 9223372036854775807, + --- Create a new table + --- @param self Counter + --- @return table + create_table = function(self) end, + --- Fetch the global counter online + --- @param self Counter + --- @param url string + --- @return string + fetch = function(self, url) end, + --- Get the current counter value + --- @param self Counter + --- @return integer + get = function(self) end, + --- Increment the counter + --- @param self Counter + increment = function(self) end, + __metatable = { + --- @param param1 userdata + --- @param param2 any + --- @return any + __index = function(param1, param2) end, + --- @param param1 userdata + --- @param param2 any + --- @param param3 any + --- @return any | nil + __newindex = function(param1, param2, param3) end, + --- String representation of the counter + --- @param self Counter + --- @return string + __tostring = function(self) end, + } +} + diff --git a/examples/types/macros.d.luau b/examples/types/macros.d.luau new file mode 100644 index 0000000..2caddb9 --- /dev/null +++ b/examples/types/macros.d.luau @@ -0,0 +1,30 @@ +-- Simple Counter +declare class Counter + -- Direction of the counter + direction: string + value: number + -- Create a new table + function create_table(self): table + -- Fetch the global counter online + function fetch(self, url: string): string + -- Get the current counter value + function get(self): number + -- Increment the counter + function increment(self): () + -- String representation of the counter + function __tostring(self): string +end + +declare function Counter___index(param1: any, param2: any): any +declare function Counter___newindex(param1: any, param2: any, param3: any): any? + +declare Counter: { + -- The default count + COUNT: number, + -- Min count value + MIN: number, + -- Max count value + max: number, + __index: typeof(Counter___index), + __newindex: typeof(Counter___newindex), +} diff --git a/mlua_extras_derive/Cargo.lock b/mlua_extras_derive/Cargo.lock index 8e01610..6c15e5a 100644 --- a/mlua_extras_derive/Cargo.lock +++ b/mlua_extras_derive/Cargo.lock @@ -75,7 +75,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" dependencies = [ "deluxe-core", - "heck", + "heck 0.4.1", "if_chain", "proc-macro-crate", "proc-macro2", @@ -101,6 +101,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "ident_case" version = "1.0.1" @@ -138,6 +144,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", + "strum", "syn 2.0.77", "venial", ] @@ -212,6 +219,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/mlua_extras_derive/Cargo.toml b/mlua_extras_derive/Cargo.toml index d5a3a43..188ede1 100644 --- a/mlua_extras_derive/Cargo.toml +++ b/mlua_extras_derive/Cargo.toml @@ -19,5 +19,6 @@ deluxe = "0.5.0" proc-macro-error = "1.0.4" proc-macro2 = "1.0.86" quote = "1.0.37" +strum = { version = "0.28.0", features = ["derive"] } syn = "2.0.77" venial = "0.6.0" diff --git a/mlua_extras_derive/src/builder.rs b/mlua_extras_derive/src/builder.rs new file mode 100644 index 0000000..e9a210e --- /dev/null +++ b/mlua_extras_derive/src/builder.rs @@ -0,0 +1,26 @@ +use std::ops::Deref; + +#[derive(strum::EnumIs)] +pub enum Kind { + Regular, + Typed, +} + +pub struct Builder { + kind: Kind +} + +impl Deref for Builder { + type Target = Kind; + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl Builder { + #[inline(always)] + pub fn regular() -> Self { Self { kind: Kind::Regular } } + + #[inline(always)] + pub fn typed() -> Self { Self { kind: Kind::Typed } } +} \ No newline at end of file diff --git a/mlua_extras_derive/src/extract.rs b/mlua_extras_derive/src/extract.rs index b526e95..1efa309 100644 --- a/mlua_extras_derive/src/extract.rs +++ b/mlua_extras_derive/src/extract.rs @@ -3,8 +3,7 @@ use deluxe::{ParseAttributes, ParseMetaItem}; use proc_macro2::{Literal, TokenStream}; use quote::ToTokens; use syn::{ - spanned::Spanned, Attribute, Expr, ExprLit, FnArg, ImplItemFn, Lit, Meta, MetaNameValue, Pat, - PatIdent, ReturnType, + Attribute, Expr, ExprLit, FnArg, ImplItemConst, ImplItemFn, Lit, Meta, MetaNameValue, Pat, PatIdent, ReturnType, spanned::Spanned }; pub fn doc_comment(attrs: &[syn::Attribute]) -> Option { @@ -73,6 +72,29 @@ pub struct UserDataField { pub rename: Option, } +impl UserDataField { + pub fn from_impl_const(field: &ImplItemConst) -> Option { + let field_attr = field + .attrs + .iter() + .find(|a| is_field_attr(a)) + .map(|a| deluxe::parse_attributes::<_, Field>(a).unwrap_or_default()) + .unwrap_or_default(); + + let docs = doc_comment(&field.attrs); + + Some(Self { + ident: Some(field.ident.clone()), + ty: field.ty.clone(), + attrs: docs, + skip: field_attr.skip, + rename: field_attr.rename.map(Index::Str), + readonly: true, + writeonly: false, + }) + } +} + #[derive(Debug, darling::FromField)] #[darling(attributes(field), forward_attrs(doc))] pub struct UserDataEnumField { @@ -82,6 +104,8 @@ pub struct UserDataEnumField { #[darling(default, skip)] pub variant: TokenStream, #[darling(default, skip)] + pub variant_name: String, + #[darling(default, skip)] pub accessor: TokenStream, #[allow(dead_code)] @@ -101,22 +125,39 @@ pub struct UserDataEnumField { #[derive(Debug)] pub enum PassBy { - Ref, - RefMut, - Value, + Ref { + #[allow(unused)] + and: syn::token::And, + #[allow(unused)] + name: syn::Ident + }, + RefMut { + #[allow(unused)] + and: syn::token::And, + mutability: syn::token::Mut, + #[allow(unused)] + name: syn::Ident + }, } impl PassBy { fn from_fn_arg(value: Option<&FnArg>) -> Option { match value { Some(FnArg::Receiver(recv)) => { - if recv.reference.is_some() { - if recv.mutability.is_some() { - Some(PassBy::RefMut) + if let Some((and, _lifetime)) = &recv.reference { + if let Some(mutability) = recv.mutability { + Some(PassBy::RefMut { + and: and.clone(), + mutability, + name: syn::Ident::new("self", recv.self_token.span()) + }) } else { - Some(PassBy::Ref) + Some(PassBy::Ref { + and: and.clone(), + name: syn::Ident::new("self", recv.self_token.span()) + }) } } else { - Some(PassBy::Value) + proc_macro_error::abort!(recv.self_token, "must be a reference"); } } Some(FnArg::Typed(typed)) => { @@ -129,13 +170,21 @@ impl PassBy { { if ident == "self" { if by_ref.is_some() { - if mutability.is_some() { - Some(PassBy::RefMut) + let and = syn::token::And(typed.ty.span()); + if let Some(mutability) = mutability { + Some(PassBy::RefMut { + and, + mutability: *mutability, + name: ident.clone(), + }) } else { - Some(PassBy::Ref) + Some(PassBy::Ref { + and, + name: ident.clone(), + }) } } else { - Some(PassBy::Value) + proc_macro_error::abort!(ident, "must be a reference"); } } else { None @@ -149,20 +198,39 @@ impl PassBy { } } -#[derive(Debug)] +#[derive(Debug, strum::EnumIs)] pub enum MethodKind { Regular, Meta, + StaticField, + Getter, + Setter, } impl MethodKind { - pub fn is_meta(&self) -> bool { + pub fn is_field(&self) -> bool { match self { - Self::Regular => false, - Self::Meta => true, + Self::Getter | Self::Setter | Self::StaticField => true, + _ => false } } + + pub fn is_attr(attr: &syn::Attribute) -> bool { + is_method_attr(attr) + || is_metamethod_attr(attr) + || is_getter_attr(attr) + || is_setter_attr(attr) + || is_field_attr(attr) + } } +#[derive(Debug, ParseAttributes)] +#[deluxe(attributes(getter))] +struct Getter(String); + +#[derive(Debug, ParseAttributes)] +#[deluxe(attributes(setter))] +struct Setter(String); + #[derive(Debug, ParseAttributes)] #[deluxe(attributes(metamethod))] struct MetaMethod(IdentOrCustom); @@ -173,6 +241,17 @@ struct Method { rename: Option, } +#[derive(Default, Debug, ParseAttributes)] +#[deluxe(default, attributes(field))] +struct Field { + skip: bool, + rename: Option, +} + +pub fn is_field_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("field") +} + pub fn is_method_attr(attr: &syn::Attribute) -> bool { attr.path().is_ident("method") } @@ -181,6 +260,14 @@ pub fn is_metamethod_attr(attr: &syn::Attribute) -> bool { attr.path().is_ident("metamethod") } +pub fn is_getter_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("getter") +} + +pub fn is_setter_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("setter") +} + #[derive(Debug)] pub struct UserDataMethod { #[allow(dead_code)] @@ -196,12 +283,19 @@ pub struct UserDataMethod { pub kind: MethodKind, } impl UserDataMethod { - pub fn from_imp_fn(method: &ImplItemFn) -> Option { + pub fn from_impl_fn(method: &ImplItemFn) -> Option { + let field_attr = method + .attrs + .iter() + .find(|a| is_field_attr(a)) + .map(|a| deluxe::parse_attributes::<_, Field>(a).unwrap_or_default()); + let method_attr = method .attrs .iter() .find(|a| is_method_attr(a)) .map(|a| deluxe::parse_attributes::<_, Method>(a).unwrap_or_default()); + let metamethod_attr = match method .attrs .iter() @@ -209,13 +303,43 @@ impl UserDataMethod { { Some(a) => match deluxe::parse_attributes::<_, MetaMethod>(a) { Ok(v) => Some(v), - Err(err) => proc_macro_error::abort!(method.span(), "{}", err), + Err(err) => proc_macro_error::abort!(method, "{}", err), }, None => None, }; - if method_attr.is_some() && metamethod_attr.is_some() { - return None; + let getter_attr = match method + .attrs + .iter() + .find(|a| is_getter_attr(a)) + { + Some(a) => match deluxe::parse_attributes::<_, Getter>(a) { + Ok(v) => Some(v), + Err(err) => proc_macro_error::abort!(method, "{}", err), + }, + None => None + }; + + let setter_attr = match method + .attrs + .iter() + .find(|a| is_setter_attr(a)) + { + Some(a) => match deluxe::parse_attributes::<_, Setter>(a) { + Ok(v) => Some(v), + Err(err) => proc_macro_error::abort!(method, "{}", err), + }, + None => None + }; + + let matches = method_attr.as_ref().map(|_| 1).unwrap_or_default() + + metamethod_attr.as_ref().map(|_| 1).unwrap_or_default() + + getter_attr.as_ref().map(|_| 1).unwrap_or_default() + + setter_attr.as_ref().map(|_| 1).unwrap_or_default() + + field_attr.as_ref().map(|_| 1).unwrap_or_default(); + + if matches > 1 { + proc_macro_error::abort!(method.sig.ident, "method cannot be registered more than once"); } let fn_name = method.sig.ident.clone(); @@ -223,9 +347,14 @@ impl UserDataMethod { let instance = PassBy::from_fn_arg(method.sig.inputs.first()); let doc = doc_comment(&method.attrs); - let (lua_name, kind) = if let Some(Method { rename }) = method_attr { + let (lua_name, kind): (TokenStream, MethodKind) = if let Some(Method { rename }) = method_attr { let name = rename.unwrap_or_else(|| fn_name.to_string()); (quote!(#name), MethodKind::Regular) + } else if let Some(Field { skip, rename }) = field_attr{ + if skip { return None; } + let name = rename.unwrap_or_else(|| fn_name.to_string()); + (quote!(#name), MethodKind::StaticField) + } else if let Some(MetaMethod(target)) = metamethod_attr { if (target.is_ident() && target == "Index") || (target == "__index") { let replace = "__usr_index"; @@ -236,6 +365,10 @@ impl UserDataMethod { } else { (quote!(#target), MethodKind::Meta) } + } else if let Some(Getter(field)) = getter_attr { + (quote!(#field), MethodKind::Getter) + } else if let Some(Setter(field)) = setter_attr { + (quote!(#field), MethodKind::Setter) } else { return None; }; @@ -250,6 +383,16 @@ impl UserDataMethod { } } + if kind.is_static_field() { + if !method.sig.inputs.is_empty() { + proc_macro_error::abort!(method.sig.inputs[0], "expeced 0 arguments"); + } + + if let ReturnType::Default = method.sig.output { + proc_macro_error::abort!(method.sig.span(), "expeced return type"); + } + } + // Collect non-self parameters let mut params_iter = method.sig.inputs.iter().peekable(); @@ -275,6 +418,14 @@ impl UserDataMethod { if let Some(FnArg::Typed(pat_type)) = params_iter.peek() { if let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat { if ident == "lua" { + // Validate whether Lua is passed by reference or not based on if the + // method is registered as async. In mlua `async` method/function variants + // pass `Lua` by value while all other methods/functions are pass by reference. + match &*pat_type.ty { + syn::Type::Reference(_) => if is_async { proc_macro_error::abort!(pat_type.ty, "cannot be a reference") }, + _ if !is_async => proc_macro_error::abort!(pat_type.ty, "must be a reference"), + _ => () + } has_lua = true; params_iter.next(); } @@ -378,7 +529,31 @@ pub enum Index { Int(isize), Str(String), } +impl ParseMetaItem for Index { + fn parse_meta_item(input: syn::parse::ParseStream, _mode: deluxe::ParseMode) -> deluxe::Result { + let lit: Lit = input.parse()?; + + match lit { + Lit::Int(int) => { + let val = int.base10_parse::()?; + Ok(Self::Int(val)) + }, + Lit::Str(s) => { + Ok(Self::Str(s.value())) + }, + _ => Err(deluxe::Error::new(lit.span(), "Expected string or integer")) + } + } +} + impl Index { + pub fn as_int(&self) -> isize { + match self { + Self::Int(v) => *v, + Self::Str(_) => 0 + } + } + pub fn is_str(&self) -> bool { match self { Self::Str(_) => true, diff --git a/mlua_extras_derive/src/lib.rs b/mlua_extras_derive/src/lib.rs index 9e159df..ee9d2b1 100644 --- a/mlua_extras_derive/src/lib.rs +++ b/mlua_extras_derive/src/lib.rs @@ -2,14 +2,14 @@ extern crate quote; use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use proc_macro_error::{proc_macro_error, abort}; -use syn::spanned::Spanned; -use venial::{Item, parse_item}; +use proc_macro_error::proc_macro_error; + +use crate::builder::Builder; mod methods; mod userdata; pub(crate) mod extract; +pub(crate) mod builder; /// Generates a [mlua::UserData] implementation from struct fields. /// @@ -62,13 +62,14 @@ pub(crate) mod extract; #[proc_macro_derive(UserData, attributes(field))] pub fn derive_user_data(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); - userdata::derive(input).into() + Builder::regular().derive_fields(input).into() } /// Attribute macro that registers methods from an `impl` block for use in Lua. /// /// Used on an `impl` block for a type that derives [`UserData`](macro@UserData), this -/// macro will register methods annotated with `#[method]` and `#[metamethod(...)]`. +/// macro will register methods annotated with `#[method]`, `#[metamethod(...)]`, `#[getter(...)]`, +/// `#[setter(...)]`, and `#[field]` along with const expressions with or without `#[field]`. /// /// # Attributes /// @@ -77,6 +78,15 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { /// - `#[metamethod(...)]` /// - `#[metamethod(ToString)]`: register as a metamethod as a [`mlua::MetaMethod`] variant /// - `#[metamethod("__custom")]`: register as a custom named metamethod +/// - `#[getter(...)]` +/// - `#[getter("field")]`: register the function as a getter for the named field +/// - `#[setter(...)]` +/// - `#[setter("field")]`: register the function as a setter for the named field +/// - `#[field(...)]` +/// - Applied to a function will call the function once to register a static field +/// - Applied to a `const` expr will register the value as a static field +/// - `#[field(rename="field")]`: register field with the custom name +/// - `#[field(skip)]`: ignore the function or `const` expr and don't register it /// /// # Patterns /// @@ -94,16 +104,38 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { /// - `T`: Method is infallible and is wrapped with `Ok(...)` when registered /// - `()`: Method is infallible and has no return value. Registration returns `Ok(())` /// -/// All methods stay as is and stay as regular callable rust functions. Any methods without `#[method]` or `#[metamethod(...)]` will not be registered. +/// All methods stay as is and stay as regular callable rust functions. Any methods without one of the listed attribute macros will not be registered. /// /// # Example /// /// ```ignore -/// #[derive(Clone, UserData)] +/// #[derive(Clone, TypedUserData)] /// struct Counter { value: i64 } /// -/// #[user_data_impl] +/// #[typed_user_data_impl] /// impl Counter { +/// const COUNT: usize = 10; +/// +/// #[field] +/// fn max() -> i64 { +/// i64::MAX +/// } +/// +/// #[field(rename = "MIN")] +/// fn min() -> i64 { +/// 0 +/// } +/// +/// #[getter("direction")] +/// fn get_direction(&self) -> String { +/// "west".into() +/// } +/// +/// #[setter("direction")] +/// fn set_direction(&mut self, dir: String) { +/// _ = dir; +/// } +/// /// #[method] /// fn get(&self) -> i64 { self.value } /// @@ -121,7 +153,8 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { /// // Requires the `async` feature /// // Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async` /// #[method] -/// async fn fetch(&self, url: String) -> mlua::Result { +/// async fn fetch(&self, lua: mlua::Lua, url: String) -> mlua::Result { +/// _ = lua; /// Ok(format!("fetched: {url}")) /// } /// } @@ -130,7 +163,7 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { let item = syn::parse_macro_input!(item as syn::ItemImpl); - methods::derive(item).into() + Builder::regular().derive_methods(item).into() } /// Generates a [`Typed`](mlua_extras::Typed) implementation from fields. @@ -152,79 +185,161 @@ pub fn user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { #[proc_macro_error] #[proc_macro_derive(Typed)] pub fn derive_typed(input: TokenStream) -> TokenStream { - let input = TokenStream2::from(input); - match parse_item(input.clone()) { - Ok(Item::Struct(struct_type)) => { - let name = struct_type.name.clone(); - let label = name.to_string(); - quote!( - impl mlua_extras::typed::Typed for #name { - fn ty() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::class(mlua_extras::typed::TypedClassBuilder::new::<#name>()) - } - - fn as_param() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::named(#label) - } - - fn as_return() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::named(#label) - } - } - ) - }, - Ok(Item::Enum(enum_type)) => { - let name = enum_type.name.clone(); - let label = name.to_string(); - let underscore_name = format!("_{name}"); - - let named = enum_type.variants.iter().map(|(variant, _)| format!("{label}{}", variant.name)).collect::>(); - let variants = enum_type.variants - .iter() - .map(|(variant, _punc)| { - let name = format!("{label}{}", variant.name); - quote!{ - ( - #name, - mlua_extras::typed::Type::class( - mlua_extras::typed::TypedClassBuilder::default() - .derive(#underscore_name) - ) - ) - } - }) - .collect::>(); - - // TODO: This should be a union alias - quote!( - impl mlua_extras::typed::Typed for #name { - fn ty() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::r#union([ - #(mlua_extras::typed::Type::named(#named),)* - ]) - } - - fn implicit() -> impl IntoIterator { - [ - ( - #underscore_name, - mlua_extras::typed::Type::class(mlua_extras::typed::TypedClassBuilder::new::()) - ), - #(#variants,)* - ] - } - - fn as_param() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::named(#label) - } + let item = syn::parse_macro_input!(input as syn::DeriveInput); + Builder::derive_typed(&item, false).into() +} - fn as_return() -> mlua_extras::typed::Type { - mlua_extras::typed::Type::named(#label) - } - } - ) - }, - Err(err) => abort!(err.span(), "{}", err), - _ => abort!(input.span(), "only `struct` and `enum` types are supported for Typed") - }.into() +/// Generates a [mlua_extras::typed::TypedUserData] implementation from struct fields. +/// +/// Each named and unnamed field is automatically exposed to Lua as a read and/or write property or index. +/// +/// Use `#[field(...)]` attributes to controll access and naming: +/// +/// - `readonly`: Set the field to only be readable within Lua +/// - `writeonly`: Set the field to only be writable within Lua +/// - `skip`: Ignore generating and exposing the field +/// - `rename`: Rename the field to a string for a named field and a digit for an indexed field +/// +/// > Note: `readonly` + `writeonly` together is the same as having neither, the field will be exposed +/// > for both read and write. +/// +/// Optionally combine with [`macro@typed_user_data_impl`] to also register methods in a rust like manner. +/// +/// # Example +/// +/// ```ignore +/// #[derive(Clone, TypedUserData)] +/// struct Player { +/// name: String, +/// health: f64, +/// #[field(skip)] +/// handle: u64, +/// #[field(readonly)] +/// score: i32, +/// #[field(rename = "pos_x")] +/// position_x: f64, +/// } +/// ``` +/// +/// ```ignore +/// #[derive(Clone, TypedUserData)] +/// enum PlayerAction { +/// Idle, +/// Move { +/// x: i32, +/// y: i32 +/// }, +/// Attack( +/// #[field(rename = "name")] +/// String +/// ), +/// Quit, +/// } +/// ``` +#[proc_macro_error] +#[proc_macro_derive(TypedUserData, attributes(field))] +pub fn derive_typed_user_data(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + Builder::typed().derive_fields(input).into() } + +/// Attribute macro that registers methods from an `impl` block for use in Lua. +/// +/// Used on an `impl` block for a type that derives [`TypedUserData`](macro@TypedUserData), this +/// macro will register methods annotated with `#[method]`, `#[metamethod(...)]`, `#[getter(...)]`, +/// `#[setter(...)]`, and `#[field]` along with const expressions with or without `#[field]`. +/// +/// # Attributes +/// +/// - `#[method]` +/// - `#[method(rename = "name")`: register as a method with the provided name +/// - `#[metamethod(...)]` +/// - `#[metamethod(ToString)]`: register as a metamethod as a [`mlua::MetaMethod`] variant +/// - `#[metamethod("__custom")]`: register as a custom named metamethod +/// - `#[getter(...)]` +/// - `#[getter("field")]`: register the function as a getter for the named field +/// - `#[setter(...)]` +/// - `#[setter("field")]`: register the function as a setter for the named field +/// - `#[field(...)]` +/// - Applied to a function will call the function once to register a static field +/// - Applied to a `const` expr will register the value as a static field +/// - `#[field(rename="field")]`: register field with the custom name +/// - `#[field(skip)]`: ignore the function or `const` expr and don't register it +/// +/// # Patterns +/// +/// - `&self`: registered with `TypedDataMethods::add_method` or `TypedDataMethods::add_meta_method` +/// - `&mut self`: registered with `TypedDataMethods::add_method_mut` or `TypedDataMethods::add_meta_method_mut` +/// - without `self`: registered with `TypedDataMethods::add_function` or `TypedDataMethods::add_meta_function` +/// - `async fn`: registered with the `TypedDataMethods::add_async_*` variant that matches the above arguments +/// - If the first non `self` parameter is `lua` then `&mlua::Lua` is passed to non async methods/functions +/// and `mlua::Lua` is passed into async methods/functions +/// +/// # Return +/// +/// - `Result` where `E: Into`: Method is fallible and the error is automatically converted to a [`mlua::Error`]. +/// This includes any error type that implements [`mlua::ExternalError`] and any return type that has the name `Result`. +/// - `T`: Method is infallible and is wrapped with `Ok(...)` when registered +/// - `()`: Method is infallible and has no return value. Registration returns `Ok(())` +/// +/// All methods stay as is and stay as regular callable rust functions. Any methods without one of the listed attribute macros will not be registered. +/// +/// # Example +/// +/// ```ignore +/// #[derive(Clone, TypedUserData)] +/// struct Counter { value: i64 } +/// +/// #[typed_user_data_impl] +/// impl Counter { +/// const COUNT: usize = 10; +/// +/// #[field] +/// fn max() -> i64 { +/// i64::MAX +/// } +/// +/// #[field(rename = "MIN")] +/// fn min() -> i64 { +/// 0 +/// } +/// +/// #[getter("direction")] +/// fn get_direction(&self) -> String { +/// "west".into() +/// } +/// +/// #[setter("direction")] +/// fn set_direction(&mut self, dir: String) { +/// _ = dir; +/// } +/// +/// #[method] +/// fn get(&self) -> i64 { self.value } +/// +/// #[method] +/// fn increment(&mut self) { self.value += 1 } +/// +/// #[method] +/// fn create_table(&self, lua: &mlua::Lua) -> mlua::Result { +/// lua.create_table() +/// } +/// +/// #[metamethod(ToString)] +/// fn to_string(&self) -> String { format!("Counter({})", self.value) } +/// +/// // Requires the `async` feature +/// // Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async` +/// #[method] +/// async fn fetch(&self, lua: mlua::Lua, url: String) -> mlua::Result { +/// _ = lua; +/// Ok(format!("fetched: {url}")) +/// } +/// } +/// ``` +#[proc_macro_error] +#[proc_macro_attribute] +pub fn typed_user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item = syn::parse_macro_input!(item as syn::ItemImpl); + Builder::typed().derive_methods(item).into() +} \ No newline at end of file diff --git a/mlua_extras_derive/src/methods.rs b/mlua_extras_derive/src/methods.rs index d1e0ca8..e28c6de 100644 --- a/mlua_extras_derive/src/methods.rs +++ b/mlua_extras_derive/src/methods.rs @@ -2,187 +2,382 @@ use proc_macro2::TokenStream; use syn::{ImplItem, ItemImpl, Type}; use quote::quote; -use crate::extract::{PassBy, UserDataMethod, is_metamethod_attr, is_method_attr}; +use crate::{builder::Builder, extract::{MethodKind, PassBy, UserDataField, UserDataMethod, is_field_attr}}; -pub fn derive(item: ItemImpl) -> TokenStream { - let self_ty = &item.self_ty; +impl Builder { + pub fn derive_methods(&self, item: ItemImpl) -> TokenStream { + let self_ty = &item.self_ty; - let mut user_data = Vec::new(); - let mut cleaned_items = Vec::new(); + let mut user_data_methods = Vec::new(); + let mut user_data_fields = Vec::new(); - for impl_item in &item.items { - match impl_item { - ImplItem::Fn(method) => if let Some(udm) = UserDataMethod::from_imp_fn(method) { - user_data.push(udm); + let mut cleaned_items = Vec::new(); - let mut cleaned = method.clone(); - cleaned.attrs.retain(|a| !is_method_attr(a) && !is_metamethod_attr(a)); - cleaned_items.push(ImplItem::Fn(cleaned)); - } else { - cleaned_items.push(impl_item.clone()); - } - _ => { - cleaned_items.push(impl_item.clone()); - } - } - } + for impl_item in &item.items { + match impl_item { + ImplItem::Fn(method) => if let Some(udm) = UserDataMethod::from_impl_fn(method) { + user_data_methods.push(udm); - let registrations: Vec<_> = user_data - .iter() - .map(|info| generate_registration(info, self_ty)) - .collect(); - - // Reconstruct the cleaned impl block - let attrs = &item.attrs; - let unsafety = &item.unsafety; - let impl_token = &item.impl_token; - let generics = &item.generics; - - quote! { - #(#attrs)* - #unsafety #impl_token #generics #self_ty { - #(#cleaned_items)* - } + let mut cleaned = method.clone(); + cleaned.attrs.retain(|a| !MethodKind::is_attr(a)); + cleaned_items.push(ImplItem::Fn(cleaned)); + } else { + cleaned_items.push(impl_item.clone()); + } + ImplItem::Const(const_expr) => if let Some(udf) = UserDataField::from_impl_const(const_expr) { + user_data_fields.push(udf); - impl #generics #self_ty { - #[doc(hidden)] - fn __auto_add_methods>(methods: &mut M) { - #(#registrations)* + let mut cleaned = const_expr.clone(); + cleaned.attrs.retain(|a| !is_field_attr(a)); + cleaned_items.push(ImplItem::Const(cleaned)); + } else { + cleaned_items.push(impl_item.clone()); + }, + _ => { + cleaned_items.push(impl_item.clone()); + } } } - } -} - -fn generate_registration(info: &UserDataMethod, self_ty: &Type) -> TokenStream { - let fn_name = &info.name; - let lua_name = &info.lua_name; - - let param_names: Vec<_> = info.params.iter().map(|(name, _)| name).collect(); - let param_types: Vec<_> = info.params.iter().map(|(_, ty)| ty).collect(); - - // Build the parameter destructuring for the closure - let params_destructure = if param_names.is_empty() { - quote! { _: () } - } else { - quote! { (#(#param_names,)*): (#(#param_types,)*) } - }; - - // Build the method call arguments - let call_args = if info.lua { - let args = ¶m_names; - quote! { lua, #(#args,)* } - } else { - let args = ¶m_names; - quote! { #(#args,)* } - }; - - let lua_ident = if info.lua { - quote! { lua } - } else { - quote! { _lua } - }; - - // Build the method call and return wrapping - let build_call_and_return = |call: TokenStream| -> TokenStream { - if info.fallible { - quote! { #call.map_err(|e| e.into()) } - } else if info.returnable { + + let method_registrations: Vec<_> = user_data_methods + .iter() + .filter(|i| !i.kind.is_field()) + .map(|info| self.generate_method_registration(info, self_ty)) + .collect(); + + let field_registrations: Vec<_> = user_data_methods + .iter() + .filter(|i| i.kind.is_field()) + .map(|info| self.generate_field_registration(info)) + .collect(); + + let static_field_registration: Vec<_> = user_data_fields + .iter() + .map(|info| self.generate_static_field_registration(info)) + .collect(); + + // Reconstruct the cleaned impl block + let attrs = &item.attrs; + let unsafety = &item.unsafety; + let impl_token = &item.impl_token; + let generics = &item.generics; + + if self.is_typed() { quote! { - let result = #call; - Ok(result) + #(#attrs)* + #unsafety #impl_token #generics #self_ty { + #(#cleaned_items)* + } + + impl #generics #self_ty { + #[doc(hidden)] + fn __auto_add_fields>(fields: &mut F) { + #(#static_field_registration)* + #(#field_registrations)* + } + + #[doc(hidden)] + fn __auto_add_methods>(methods: &mut M) { + #(#method_registrations)* + } + } } } else { quote! { - #call; - Ok(()) + #(#attrs)* + #unsafety #impl_token #generics #self_ty { + #(#cleaned_items)* + } + + impl #generics #self_ty { + #[doc(hidden)] + fn __auto_add_fields>(fields: &mut F) { + #(#static_field_registration)* + #(#field_registrations)* + } + + #[doc(hidden)] + fn __auto_add_methods>(methods: &mut M) { + #(#method_registrations)* + } + } } } - }; + } - let is_meta = info.kind.is_meta(); + fn generate_method_registration(&self,info: &UserDataMethod, self_ty: &Type) -> TokenStream { + let fn_name = &info.name; + let lua_name = &info.lua_name; - if info.r#async { - // Async methods - match &info.instance { - Some(PassBy::Ref|PassBy::Value) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + let param_names: Vec<_> = info.params.iter().map(|(name, _)| name).collect(); + let param_types: Vec<_> = info.params.iter().map(|(_, ty)| ty).collect(); + + // Build the parameter destructuring for the closure + let params_destructure = if param_names.is_empty() { + quote! { _: () } + } else { + quote! { (#(#param_names,)*): (#(#param_types,)*) } + }; + + // Build the method call arguments + let call_args = if info.lua { + let args = ¶m_names; + quote! { lua, #(#args,)* } + } else { + let args = ¶m_names; + quote! { #(#args,)* } + }; + + let lua_ident = if info.lua { + quote! { lua } + } else { + quote! { _lua } + }; + + // Build the method call and return wrapping + let build_call_and_return = |call: TokenStream| -> TokenStream { + if info.fallible { + quote! { #call.map_err(|e| e.into()) } + } else if info.returnable { quote! { - methods.add_async_method(#lua_name, |#lua_ident, this, #params_destructure| async move { - #body - }); + let result = #call; + Ok(result) } - } - Some(PassBy::RefMut) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + } else { quote! { - methods.add_async_method_mut(#lua_name, |#lua_ident, this, #params_destructure| async move { - #body - }); + #call; + Ok(()) } } - None => { - let body = - build_call_and_return(quote! { #self_ty::#fn_name(#call_args).await }); - quote! { - methods.add_async_function(#lua_name, |#lua_ident, #params_destructure| async move { - #body - }); + }; + + let is_meta = info.kind.is_meta(); + + let doc_stmt = self.is_typed() + .then(|| { + info.doc.as_ref().map(|doc| { + quote! { methods.document(#doc); } + }) + }); + + let param_stmts: Vec<_> = if self.is_typed() { + info.params.iter().map(|(name, _)| { + let name_str = name.to_string(); + // TODO: Update this to parse param docs somehow + quote! { methods.param(#name_str, ()); } + }).collect() + } else { + Default::default() + }; + + if info.r#async { + // Async methods + match &info.instance { + Some(PassBy::Ref{..}) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_async_method(#lua_name, |#lua_ident, this, #params_destructure| async move { + #body + }); + } + } + Some(PassBy::RefMut{..}) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_async_method_mut(#lua_name, |#lua_ident, this, #params_destructure| async move { + #body + }); + } + } + None => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args).await }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_async_function(#lua_name, |#lua_ident, #params_destructure| async move { + #body + }); + } } } - } - } else { - // Sync methods - match (&info.instance, is_meta) { - (Some(PassBy::Ref|PassBy::Value), false) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); - quote! { - methods.add_method(#lua_name, |#lua_ident, this, #params_destructure| { - #body - }); + } else { + // Sync methods + match (&info.instance, is_meta) { + (Some(PassBy::Ref{..}), false) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_method(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::Ref{..}), true) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_meta_method(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::RefMut{..}), false) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::RefMut{..}), true) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_meta_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (None, false) => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_function(#lua_name, |#lua_ident, #params_destructure| { + #body + }); + } + } + (None, true) => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + quote! { + #doc_stmt + #(#param_stmts)* + methods.add_meta_function(#lua_name, |#lua_ident, #params_destructure| { + #body + }); + } } } - (Some(PassBy::Ref|PassBy::Value), true) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + } + } + + fn generate_static_field_registration(&self, info: &UserDataField) -> TokenStream { + let name = info.ident.as_ref().unwrap(); + let lua_name = info.rename.clone().map(|v| v.to_string()).unwrap_or_else(|| name.to_string()); + + let doc_stmt = self.is_typed().then(|| { + info.attrs.as_ref().map(|doc| { + quote! { fields.document(#doc); } + }) + }); + + quote! { + #doc_stmt + fields.add_field(#lua_name, Self::#name); + } + } + + fn generate_field_registration(&self, info: &UserDataMethod) -> TokenStream { + let fn_name = &info.name; + let lua_name = &info.lua_name; + + let param_names: Vec<_> = info.params.iter().map(|(name, _)| name).collect(); + let param_types: Vec<_> = info.params.iter().map(|(_, ty)| ty).collect(); + + // Build the parameter destructuring for the closure + let params_destructure = if param_names.is_empty() { + quote! { _: () } + } else if param_names.len() == 1 { + quote! { #(#param_names)*: #(#param_types)* } + } else { + quote! { (#(#param_names,)*): (#(#param_types,)*) } + }; + + // Build the method call arguments + let call_args = if info.lua { + let args = ¶m_names; + quote! { lua, #(#args,)* } + } else { + let args = ¶m_names; + quote! { #(#args,)* } + }; + + let lua_ident = if info.lua { + quote! { lua } + } else { + quote! { _lua } + }; + + // Build the method call and return wrapping + let build_call_and_return = |call: TokenStream| -> TokenStream { + if info.fallible { + quote! { #call.map_err(|e| e.into()) } + } else if info.returnable { quote! { - methods.add_meta_method(#lua_name, |#lua_ident, this, #params_destructure| { - #body - }); + let result = #call; + Ok(result) } - } - (Some(PassBy::RefMut), false) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + } else { quote! { - methods.add_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { - #body - }); + #call; + Ok(()) } } - (Some(PassBy::RefMut), true) => { - let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + }; + + let doc_stmt = self.is_typed().then(|| { + info.doc.as_ref().map(|doc| { + quote! { fields.document(#doc); } + }) + }); + + // Sync methods + match (&info.instance, &info.kind, param_names.len()) { + (None, MethodKind::StaticField, 0) => { quote! { - methods.add_meta_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { - #body - }); + #doc_stmt + fields.add_field(#lua_name, Self::#fn_name()); } - } - (None, false) => { - let body = - build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + }, + (Some(PassBy::RefMut{..}|PassBy::Ref{..}), MethodKind::Setter, 1) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); quote! { - methods.add_function(#lua_name, |#lua_ident, #params_destructure| { + #doc_stmt + fields.add_field_method_set(#lua_name, |#lua_ident, this, #params_destructure| { #body }); } } - (None, true) => { - let body = - build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + (Some(PassBy::Ref{..}), MethodKind::Getter, 0) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); quote! { - methods.add_meta_function(#lua_name, |#lua_ident, #params_destructure| { + #doc_stmt + fields.add_field_method_get(#lua_name, |#lua_ident, this| { #body }); } } + // TODO: Parse the PassBy content to get better error location + (None, _, _) => proc_macro_error::abort!(info.name, "missing 'self'"), + (Some(PassBy::RefMut{ mutability, ..}), MethodKind::Getter, _) => proc_macro_error::abort!(mutability, "cannot be mutable"), + (_, MethodKind::Getter, len) if len != 0 => proc_macro_error::abort!(info.params[0].0, "expected 0 arguments"), + (_, MethodKind::Getter, _) => proc_macro_error::abort!(info.name, "invalid arguments"), + (_, MethodKind::Setter, len) if len != 1 => proc_macro_error::abort!(info.name, "expected 1 argument"), + (_, MethodKind::Setter, _) => proc_macro_error::abort!(info.name, "invalid arguments"), + _ => quote!() } } } \ No newline at end of file diff --git a/mlua_extras_derive/src/userdata.rs b/mlua_extras_derive/src/userdata.rs index 9e43d2f..0584a22 100644 --- a/mlua_extras_derive/src/userdata.rs +++ b/mlua_extras_derive/src/userdata.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, btree_map::Entry}; +use std::collections::{BTreeMap, BTreeSet, btree_map::Entry}; use darling::FromField; use proc_macro::Literal; @@ -6,529 +6,834 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; use syn::{Data, DeriveInput, Fields, LitInt, spanned::Spanned}; use quote::quote; -use crate::extract::{Index, UserDataEnumField, UserDataField}; +use crate::{Builder, extract::{Index, UserDataEnumField, UserDataField, doc_comment}}; -pub fn derive(input: DeriveInput) -> TokenStream2 { - let name = &input.ident; +impl Builder { + pub fn derive_fields(&self, input: DeriveInput) -> TokenStream2 { + let name = &input.ident; - match &input.data { - Data::Struct(data) => { - let fields: Vec<_> = match &data.fields { - Fields::Named(named) => named.named.iter().collect(), - Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(), - Fields::Unit => Vec::new(), - }; + let (field_registrations, method_registrations) = match &input.data { + Data::Struct(data) => { + let fields: Vec<_> = match &data.fields { + Fields::Named(named) => named.named.iter().collect(), + Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(), + Fields::Unit => Vec::new(), + }; - let user_fields = fields - .iter() - .map(|field| { - match UserDataField::from_field(field) { - Ok(uf) => uf, - Err(err) => proc_macro_error::abort!(field, "{}", err) + let user_fields = fields + .iter() + .map(|field| { + match UserDataField::from_field(field) { + Ok(uf) => uf, + Err(err) => proc_macro_error::abort!(field, "{}", err) + } + }) + .collect::>(); + + self.derive_struct(user_fields) + }, + Data::Enum(data) => { + let mut enum_fields: BTreeMap> = Default::default(); + let mut variants = Vec::new(); + + for variant in data.variants.iter() { + let vn = &variant.ident; + let (variant, fields) = match &variant.fields { + Fields::Named(named) => { + let fields = named.named.iter().filter_map(|v| v.ident.as_ref().map(|v| { + let n = format_ident!("_{v}"); + quote!(#v: #n) + })); + + variants.push((quote!(#vn{ .. }), vn)); + (quote!(#vn{ #(#fields,)* }), named.named.iter().collect()) + }, + Fields::Unnamed(unnamed) => { + let fields = (0..unnamed.unnamed.len()).map(|v| format_ident!("_{v}")); + + let v = quote!(#vn( #(#fields,)* )); + variants.push((v.clone(), vn)); + (v, unnamed.unnamed.iter().collect()) + }, + Fields::Unit => { + let v = quote!(#vn); + variants.push((v.clone(), vn)); + (v, Vec::new()) + } + }; + + for (i, field) in fields.iter().enumerate() { + match UserDataEnumField::from_field(field) { + Ok(mut uf) => { + if uf.skip { + continue; + } + + uf.variant = variant.clone(); + uf.accessor = match uf.ident.as_ref() { + Some(ident) => { + let i = format_ident!("_{ident}"); + quote!(#i) + }, + None => { + let i = format_ident!("_{i}"); + quote!(#i) + } + }; + + let idx = match uf.rename.clone().or_else(|| uf.ident.as_ref().map(|v| Index::Str(v.to_string()))) { + Some(n) => n, + None => Index::Int(i as isize + 1) + }; + + match enum_fields.entry(idx) { + Entry::Occupied(mut entry) => entry.get_mut().push(uf), + Entry::Vacant(entry) => { entry.insert(vec![uf]); } + } + }, + Err(err) => proc_macro_error::abort!(field, "{}", err) + } } - }) - .collect::>(); - - derive_struct(name, user_fields) - }, - Data::Enum(data) => { - let mut enum_fields: BTreeMap> = Default::default(); - let mut variants = Vec::new(); - - for variant in data.variants.iter() { - let vn = &variant.ident; - let (variant, fields) = match &variant.fields { - Fields::Named(named) => { - let fields = named.named.iter().filter_map(|v| v.ident.as_ref().map(|v| { - let n = format_ident!("_{v}"); - quote!(#v: #n) - })); - - variants.push((quote!(#vn{ .. }), vn)); - (quote!(#vn{ #(#fields,)* }), named.named.iter().collect()) - }, - Fields::Unnamed(unnamed) => { - let fields = (0..unnamed.unnamed.len()).map(|v| format_ident!("_{v}")); + } - let v = quote!(#vn( #(#fields,)* )); - variants.push((v.clone(), vn)); - (v, unnamed.unnamed.iter().collect()) - }, - Fields::Unit => { - let v = quote!(#vn); - variants.push((v.clone(), vn)); - (v, Vec::new()) + self.derive_enum(variants, enum_fields) + } + Data::Union(_) => { + proc_macro_error::abort!(name, "TypedUserData does not support unions"); + } + }; + + if self.is_typed() { + let typed_impl = Self::derive_typed(&input, true); + + let doc_stmt = doc_comment(&input.attrs).map(|d| quote!(docs.add(#d);)); + + quote!{ + impl #name { + #[doc(hidden)] + fn __implicit_fields>(fields: &mut F) { + #(#field_registrations)* } - }; - for (i, field) in fields.iter().enumerate() { - match UserDataEnumField::from_field(field) { - Ok(mut uf) => { - if uf.skip { - continue; - } + #[doc(hidden)] + fn __implicit_methods>(methods: &mut M) { + #(#method_registrations)* + } + } - uf.variant = variant.clone(); - uf.accessor = match uf.ident.as_ref() { - Some(ident) => { - let i = format_ident!("_{ident}"); - quote!(#i) - }, - None => { - let i = format_ident!("_{i}"); - quote!(#i) - } - }; + impl mlua_extras::typed::TypedUserData for #name { + fn add_documentation>(docs: &mut D) { + #doc_stmt + } - let idx = match uf.rename.clone().or_else(|| uf.ident.as_ref().map(|v| Index::Str(v.to_string()))) { - Some(n) => n, - None => Index::Int(i as isize + 1) - }; + fn add_fields>(fields: &mut F) { + Self::__implicit_fields(fields); - match enum_fields.entry(idx) { - Entry::Occupied(mut entry) => entry.get_mut().push(uf), - Entry::Vacant(entry) => { entry.insert(vec![uf]); } - } - }, - Err(err) => proc_macro_error::abort!(field, "{}", err) + use mlua_extras::__DefaultAutoFields as _; + Self::__auto_add_fields(fields); + } + + fn add_methods>(methods: &mut M) { + Self::__implicit_methods(methods); + + use mlua_extras::__DefaultAutoMethods as _; + Self::__auto_add_methods(methods); } } + + impl mlua_extras::mlua::UserData for #name { + fn add_fields>(fields: &mut F) { + let mut wrapper = mlua_extras::typed::WrappedBuilder::new(fields); + ::add_fields(&mut wrapper); + } + + fn add_methods>(methods: &mut M) { + let mut wrapper = mlua_extras::typed::WrappedBuilder::new(methods); + ::add_methods(&mut wrapper); + } + } + + #typed_impl } + } else { + quote! { + impl #name { + #[doc(hidden)] + fn __implicit_fields>(fields: &mut F) { + #(#field_registrations)* + } + #[doc(hidden)] + fn __implicit_methods>(methods: &mut M) { + #(#method_registrations)* + } + } - derive_enum(name, variants, enum_fields) - } - Data::Union(_) => { - proc_macro_error::abort!(name, "TypedUserData does not support unions"); - } - } -} - -fn derive_struct(name: &syn::Ident, user_fields: Vec) -> TokenStream2 { - let field_registrations = user_fields - .iter() - .enumerate() - .filter_map(|(i, fi)| { - let (index, field_ident) = match (&fi.rename, &fi.ident) { - (None, None) | (Some(Index::Int(_)), _) => return None, - (Some(Index::Str(v)), _) => (Index::Str(v.clone()), match &fi.ident { - Some(i) => quote!(#i), - None => { - let i = LitInt::new(&Literal::usize_unsuffixed(i).to_string(), fi.ty.span()); - quote!(#i) + impl mlua_extras::mlua::UserData for #name { + fn add_fields>(fields: &mut F) { + Self::__implicit_fields(fields); + + use mlua_extras::__DefaultAutoFields as _; + Self::__auto_add_fields(fields); } - }), - (None, Some(ident)) => (Index::Str(ident.to_string()), quote!(#ident)), - }; - - let field_ty = &fi.ty; - - match (fi.skip, fi.readonly, fi.writeonly) { - (true, _, _) => None, - (_, true, true) | (_, false, false) => Some(quote! { - mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( - fields, - #index, - |_lua, this| Ok(this.#field_ident.clone()), - |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, - ); - }), - (_, true, false) => Some(quote! { - fields.add_field_method_get( - #index, - |_lua, this| Ok(this.#field_ident.clone()), - ); - }), - (_, false, true) => Some(quote! { - fields.add_field_method_set( - #index, - |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, - ); - }), + + fn add_methods>(methods: &mut M) { + Self::__implicit_methods(methods); + + use mlua_extras::__DefaultAutoMethods as _; + Self::__auto_add_methods(methods); + } + } } - }); + } + } + + pub fn derive_typed(input: &syn::DeriveInput, organize_fields: bool) -> TokenStream2 { + let name = &input.ident; + match &input.data { + Data::Struct(_) => { + let label = name.to_string(); + quote!( + impl mlua_extras::typed::Typed for #name { + fn ty() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::class(mlua_extras::typed::TypedClassBuilder::new::<#name>().build()) + } - let mut method_registrations = Vec::::new(); + fn as_param() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::named(#label) + } - // Add a custom __index and __newindex for the tuple struct/enum fields - // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] - { - let indexes = user_fields - .iter() - .enumerate() - .filter_map(|(i, f)| { - match f { - UserDataField { skip: true, .. } - | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } - | UserDataField { rename: Some(Index::Str(_)), .. } - | UserDataField { readonly: false, writeonly: true, .. } => None, - UserDataField { readonly: true, writeonly: true, .. } - | UserDataField { readonly: false, writeonly: false, .. } - | UserDataField { readonly: true, writeonly: false, .. } => { - let idx = match &f.ident { - Some(i) => quote!(#i), - None => { - let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); - quote!(#i) + fn as_return() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::named(#label) + } + } + ) + }, + Data::Enum(enum_type) => { + let label = name.to_string(); + let underscore_name = format!("_{name}"); + + let named = enum_type.variants.iter().map(|variant| format!("{label}{}", variant.ident)).collect::>(); + let mut skipped_fields = BTreeSet::new(); + let variants = enum_type.variants + .iter() + .map(|variant| { + let fields = if organize_fields { + match &variant.fields { + Fields::Unit => Vec::new(), + Fields::Unnamed(fields) => { + fields.unnamed + .iter() + .enumerate() + .map(|(i, f)| { + let idx = LitInt::new(&Literal::isize_unsuffixed(i as isize + 1).to_string(), f.span()); + let ty = &f.ty; + let doc = match doc_comment(&f.attrs) { + Some(doc) => quote!(#doc), + None => quote!(()) + }; + + skipped_fields.insert(Index::Int(i as isize + 1)); + + quote!(.field(#idx, <#ty as mlua_extras::typed::Typed>::ty(), #doc)) + }) + .collect() + } + Fields::Named(fields) => { + fields.named + .iter() + .map(|f| { + let idx = f.ident.as_ref().unwrap().to_string(); + let ty = &f.ty; + let doc = match doc_comment(&f.attrs) { + Some(doc) => quote!(#doc), + None => quote!(()) + }; + + skipped_fields.insert(Index::Str(idx.clone())); + + quote!(.field(#idx, <#ty as mlua_extras::typed::Typed>::ty(), #doc)) + }) + .collect() + } } + } else { + Vec::new() }; - let lua_idx = LitInt::new(&Literal::isize_unsuffixed(match f.rename { - Some(Index::Int(v)) => v, - _ => i as isize + 1 - }).to_string(), Span::call_site()); - Some(quote!(Some(#lua_idx) => return mlua_extras::mlua::IntoLua::into_lua(this.#idx.clone(), _lua),)) - }, - } - }).collect::>(); + let name = format!("{label}{}", variant.ident); + quote!{ + ( + #name, + mlua_extras::typed::Type::class( + mlua_extras::typed::TypedClassBuilder::default() + .derive(#underscore_name) + #(#fields)* + .build() + ) + ) + } + }) + .collect::>(); - let new_indexes = user_fields + let skipped_fields = skipped_fields + .iter() + .map(|i| quote!(.skip_field(#i))); + + quote!( + impl mlua_extras::typed::Typed for #name { + fn ty() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::r#union([ + #(mlua_extras::typed::Type::named(#named),)* + ]) + } + + fn implicit() -> impl IntoIterator { + [ + ( + #underscore_name, + mlua_extras::typed::Type::class( + mlua_extras::typed::TypedClassBuilder::new::() + #(#skipped_fields)* + .build() + ) + ), + #(#variants,)* + ] + } + + fn as_param() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::named(#label) + } + + fn as_return() -> mlua_extras::typed::Type { + mlua_extras::typed::Type::named(#label) + } + } + ) + }, + _ => proc_macro_error::abort!(input, "only `struct` and `enum` types are supported for Typed") + }.into() + } + + fn derive_struct(&self, user_fields: Vec) -> (Vec, Vec) { + let field_registrations: Vec<_> = user_fields .iter() .enumerate() - .filter_map(|(i, f)| { - match f { - UserDataField { skip: true, .. } - | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } - | UserDataField { rename: Some(Index::Str(_)), .. } - | UserDataField { readonly: true, writeonly: false, .. } => None, - UserDataField { readonly: true, writeonly: true, .. } - | UserDataField { readonly: false, writeonly: false, .. } - | UserDataField { readonly: false, writeonly: true, .. } => { - let idx = match &f.ident { - Some(i) => quote!(#i), - None => { - let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); - quote!(#i) - } - }; - let lua_idx = Some(LitInt::new(&Literal::isize_unsuffixed(match f.rename { - Some(Index::Int(v)) => v, - _ => i as isize + 1 - }).to_string(), Span::call_site())); - let ty = &f.ty; - - Some(quote!(Some(#lua_idx) => { - this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; - return Ok(None) - },)) - }, + .filter_map(|(i, fi)| { + let (index, field_ident) = match (&fi.rename, &fi.ident) { + (None, None) | (Some(Index::Int(_)), _) => return None, + (Some(Index::Str(v)), _) => (Index::Str(v.clone()), match &fi.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::usize_unsuffixed(i).to_string(), fi.ty.span()); + quote!(#i) + } + }), + (None, Some(ident)) => (Index::Str(ident.to_string()), quote!(#ident)), + }; + + let field_ty = &fi.ty; + + let doc_stmt = self.is_typed().then(|| { + fi.attrs.as_ref().map(|doc| { + quote! { fields.document(#doc); } + }) + }); + + match (fi.skip, fi.readonly, fi.writeonly) { + (true, _, _) => None, + (_, true, true) | (_, false, false) => Some(if self.is_regular() { + quote! { + mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( + fields, + #index, + |_lua, this| Ok(this.#field_ident.clone()), + |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, + ); + } + } else { + quote! { + #doc_stmt + fields.add_field_method_get_set( + #index, + |_lua, this| Ok(this.#field_ident.clone()), + |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, + ); + } + }), + (_, true, false) => Some(quote! { + #doc_stmt + fields.add_field_method_get( + #index, + |_lua, this| Ok(this.#field_ident.clone()), + ); + }), + (_, false, true) => Some(quote! { + #doc_stmt + fields.add_field_method_set( + #index, + |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, + ); + }), } - }).collect::>(); - - method_registrations.push(quote!{ - methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { - { - let this = this.borrow::()?; - match _idx.as_integer() { - #(#indexes)* - _ => () + }) + .collect(); + + let mut method_registrations = Vec::::new(); + + // Add a custom __index and __newindex for the tuple struct/enum fields + // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] + { + let mut index_types: BTreeMap)> = Default::default(); + + let indexes = user_fields + .iter() + .enumerate() + .filter_map(|(i, f)| { + match f { + UserDataField { skip: true, .. } + | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } + | UserDataField { rename: Some(Index::Str(_)), .. } + | UserDataField { readonly: false, writeonly: true, .. } => None, + UserDataField { readonly: true, writeonly: true, .. } + | UserDataField { readonly: false, writeonly: false, .. } + | UserDataField { readonly: true, writeonly: false, .. } => { + let idx = match &f.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); + quote!(#i) + } + }; + + let lua_idx = match f.rename { + Some(Index::Int(v)) => v, + _ => i as isize + 1 + }; + + if self.is_typed() { + index_types.insert(lua_idx, (f.ty.clone(), f.attrs.clone())); + } + + let lua_idx = LitInt::new(&Literal::isize_unsuffixed(lua_idx).to_string(), Span::call_site()); + + Some(quote!(Some(#lua_idx) => return mlua_extras::mlua::IntoLua::into_lua(this.#idx.clone(), _lua),)) + }, } - } + }).collect::>(); - let metatable = this.metatable()?; - if let Ok(usr) = metatable.get::("__usr_index") { - return usr.call::((this.clone(), _idx.clone())); - } + let new_indexes = user_fields + .iter() + .enumerate() + .filter_map(|(i, f)| { + match f { + UserDataField { skip: true, .. } + | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } + | UserDataField { rename: Some(Index::Str(_)), .. } + | UserDataField { readonly: true, writeonly: false, .. } => None, + UserDataField { readonly: true, writeonly: true, .. } + | UserDataField { readonly: false, writeonly: false, .. } + | UserDataField { readonly: false, writeonly: true, .. } => { + let idx = match &f.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); + quote!(#i) + } + }; - Err(mlua_extras::mlua::Error::runtime(match _idx { - mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), - mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), - _ => "type does not contain index".into() - })) - }); + let lua_idx = match f.rename { + Some(Index::Int(v)) => v, + _ => i as isize + 1 + }; - methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { - { - let mut this = this.borrow_mut::()?; - match _idx.as_integer() { - #(#new_indexes)* - _ => () + if self.is_typed() && !index_types.contains_key(&lua_idx) { + index_types.insert(lua_idx, (f.ty.clone(), f.attrs.clone())); + } + + let lua_idx = Some(LitInt::new(&Literal::isize_unsuffixed(lua_idx).to_string(), Span::call_site())); + let ty = &f.ty; + + Some(quote!(Some(#lua_idx) => { + this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; + return Ok(None) + },)) + }, } - } + }).collect::>(); - let metatable = this.metatable()?; - if let Ok(usr) = metatable.get::("__usr_newindex") { - return usr.call::>((this.clone(), _idx.clone(), _value)); - } - - Err(mlua_extras::mlua::Error::runtime(match _idx { - mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), - mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), - _ => "type does not contain index".into() - })) - }); - }); - } + let index_types = index_types + .iter() + .map(|(k, (ty, doc))| { + let idx = LitInt::new(&Literal::isize_unsuffixed(*k).to_string(), Span::call_site()); + let doc = match doc { + Some(doc) => quote!(#doc), + None => quote!(()) + }; + quote!(methods.index_as(#idx, <#ty as mlua_extras::typed::Typed>::ty(), #doc);) + }); - quote! { - impl #name { - #[doc(hidden)] - fn __auto_add_fields>(fields: &mut F) { - #(#field_registrations)* - } - #[doc(hidden)] - fn __implicit_methods>(methods: &mut M) { - #(#method_registrations)* - } - } + method_registrations.push(quote!{ + #(#index_types)* - impl mlua_extras::mlua::UserData for #name { - fn add_fields>(fields: &mut F) { - Self::__auto_add_fields(fields); - } + methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + { + let this = this.borrow::()?; + match _idx.as_integer() { + #(#indexes)* + _ => () + } + } - fn add_methods>(methods: &mut M) { - Self::__implicit_methods(methods); + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_index") { + return usr.call::((this.clone(), _idx.clone())); + } - use mlua_extras::__DefaultAutoMethods as _; - Self::__auto_add_methods(methods); - } + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), + _ => "type does not contain index".into() + })) + }); + + methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { + { + let mut this = this.borrow_mut::()?; + match _idx.as_integer() { + #(#new_indexes)* + _ => () + } + } + + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_newindex") { + return usr.call::>((this.clone(), _idx.clone(), _value)); + } + + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), + _ => "type does not contain index".into() + })) + }); + }); } + + (field_registrations, method_registrations) } -} -fn derive_enum(name: &syn::Ident, enum_variants: Vec<(TokenStream2, &syn::Ident)>, user_fields: BTreeMap>) -> TokenStream2 { - let count = enum_variants.len(); + fn derive_enum(&self, enum_variants: Vec<(TokenStream2, &syn::Ident)>, user_fields: BTreeMap>) -> (Vec, Vec) { + let count = enum_variants.len(); - let field_registrations = user_fields - .iter() - .filter(|(idx, _fi)| idx.is_str()) - .map(|(idx, fi)| { - let has_get = fi.iter().any(|f| f.readonly || (!f.readonly && !f.writeonly)); - let has_set = fi.iter().any(|f| f.writeonly || (!f.readonly && !f.writeonly)); + let mut field_registrations: Vec<_> = user_fields + .iter() + .filter(|(idx, _fi)| idx.is_str()) + .map(|(idx, fi)| { + let has_get = fi.iter().any(|f| f.readonly || (!f.readonly && !f.writeonly)); + let has_set = fi.iter().any(|f| f.writeonly || (!f.readonly && !f.writeonly)); + + let getter = { + let variants: Vec<_> = fi + .iter() + .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) + .map(|v| { + let variant = &v.variant; + let accessor = &v.accessor; + quote!(Self::#variant => #accessor.clone(),) + }) + .collect(); + + let err_msg = format!("type variant does not contain field '{idx}'"); + let catchall = if variants.len() < count { + quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } + } else { + quote!() + }; + + quote!(Ok(match this { + #(#variants)* + #catchall + })) + }; - let getter = { - let variants: Vec<_> = fi - .iter() - .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) - .map(|v| { - let variant = &v.variant; - let accessor = &v.accessor; - quote!(Self::#variant => #accessor.clone(),) + let setter = { + let variants: Vec<_> = fi + .iter() + .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) + .map(|v| { + let variant = &v.variant; + let accessor = &v.accessor; + let ty = &v.ty; + quote!(Self::#variant => *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value, _lua)?,) + }) + .collect(); + + let err_msg = format!("type variant does not contain field '{idx}'"); + let catchall = if variants.len() < count { + quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } + } else { + quote!() + }; + + quote!({ + match this { + #(#variants)* + #catchall + } + Ok(()) }) - .collect(); - - let err_msg = format!("type variant does not contain field '{idx}'"); - let catchall = if variants.len() < count { - quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } - } else { - quote!() }; - quote!(Ok(match this { - #(#variants)* - #catchall - })) - }; - - let setter = { - let variants: Vec<_> = fi + let doc_stmt = fi .iter() - .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) - .map(|v| { - let variant = &v.variant; - let accessor = &v.accessor; - let ty = &v.ty; - quote!(Self::#variant => *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value, _lua)?,) + .filter_map(|f| { + f.attrs.as_deref().map(|doc| format!("**{}:** {}", f.variant_name, doc)) }) - .collect(); + .collect::>(); - let err_msg = format!("type variant does not contain field '{idx}'"); - let catchall = if variants.len() < count { - quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } - } else { + let doc_stmt = if self.is_regular() || doc_stmt.is_empty() { quote!() + } else { + let doc = doc_stmt.join("\n"); + quote! { fields.document(#doc); } }; - quote!({ - match this { - #(#variants)* - #catchall + match (has_get, has_set) { + (true, true) | (false, false) => if self.is_regular() { + quote! { + mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( + fields, + #idx, + |_lua, this| #getter, + |_lua, this, _value: mlua_extras::mlua::Value| #setter, + ); + } + } else { + quote! { + #doc_stmt + fields.add_field_method_get_set( + #idx, + |_lua, this| #getter, + |_lua, this, _value: mlua_extras::mlua::Value| #setter, + ); + } + }, + (true, false) => quote! { + #doc_stmt + fields.add_field_method_get( + #idx, + |_lua, this| #getter, + ); + }, + (false, true) => quote! { + #doc_stmt + fields.add_field_method_set( + #idx, + |_lua, this, _value: mlua_extras::mlua::Value| #setter, + ); + }, + } + }) + .collect(); + + let mut method_registrations = Vec::::new(); + + // Add a custom __index and __newindex for the tuple struct/enum fields + // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] + { + let mut index_types: BTreeMap)>> = Default::default(); + + let indexes = user_fields + .iter() + .filter(|(idx, _fi)| !idx.is_str()) + .filter_map(|(idx, f)| { + let mut types = Vec::new(); + let variants: Vec<_> = f + .iter() + .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) + .map(|f| { + let variant = &f.variant; + let accessor = &f.accessor; + + if self.is_typed() { + types.push((f.ty.clone(), f.attrs.clone().map(|doc| format!("**{}:** {}", f.variant_name, doc)))); + } + + quote!{ + Self::#variant => return mlua_extras::mlua::IntoLua::into_lua(#accessor.clone(), _lua), + } + }) + .collect(); + + if variants.is_empty() { + return None; } - Ok(()) - }) - }; - - match (has_get, has_set) { - (true, true) | (false, false) => Some(quote! { - mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( - fields, - #idx, - |_lua, this| #getter, - |_lua, this, _value: mlua_extras::mlua::Value| #setter, - ); - }), - (true, false) => Some(quote! { - fields.add_field_method_get( - #idx, - |_lua, this| #getter, - ); - }), - (false, true) => Some(quote! { - fields.add_field_method_set( - #idx, - |_lua, this, _value: mlua_extras::mlua::Value| #setter, - ); - }), - } - }); - let mut method_registrations = Vec::::new(); + if !types.is_empty() { + let i = idx.as_int(); + index_types.insert(i, types); + } - // Add a custom __index and __newindex for the tuple struct/enum fields - // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] - { - let indexes = user_fields - .iter() - .filter(|(idx, _fi)| !idx.is_str()) - .filter_map(|(idx, f)| { - let variants: Vec<_> = f - .iter() - .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) - .map(|f| { - let variant = &f.variant; - let accessor = &f.accessor; - quote!{ - Self::#variant => return mlua_extras::mlua::IntoLua::into_lua(#accessor.clone(), _lua), + let catchall = if variants.len() < count { + quote!{ _ => () } + } else { + quote!() + }; + + Some(quote! { + Some(#idx) => match &*this { + #(#variants)* + #catchall } }) - .collect(); + }).collect::>(); - if variants.is_empty() { - return None; - } + let new_indexes = user_fields + .iter() + .filter(|(idx, _fi)| !idx.is_str()) + .filter_map(|(idx, f)| { + let mut types = Vec::new(); + let variants: Vec<_> = f + .iter() + .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) + .map(|f| { + let variant = &f.variant; + let accessor = &f.accessor; + let ty = &f.ty; + + if self.is_typed() { + types.push((f.ty.clone(), f.attrs.clone().map(|doc| format!("**{}:** {}", f.variant_name, doc)))); + } - let catchall = if variants.len() < count { - quote!{ _ => () } - } else { - quote!() - }; + quote!{ + Self::#variant => { + *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; + return Ok(None); + }, + } + }) + .collect(); - Some(quote! { - Some(#idx) => match &*this { - #(#variants)* - #catchall + if variants.is_empty() { + return None; } - }) - }).collect::>(); - let new_indexes = user_fields - .iter() - .filter(|(idx, _fi)| !idx.is_str()) - .filter_map(|(idx, f)| { - let variants: Vec<_> = f - .iter() - .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) - .map(|f| { - let variant = &f.variant; - let accessor = &f.accessor; - let ty = &f.ty; - quote!{ - Self::#variant => { - *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; - return Ok(None); - }, + if !types.is_empty() { + let i = idx.as_int(); + if !index_types.contains_key(&i) { + index_types.insert(i, types); + } + } + + let catchall = if variants.len() < count { + quote!{ _ => () } + } else { + quote!() + }; + + Some(quote! { + Some(#idx) => match &mut *this { + #(#variants)* + #catchall } }) - .collect(); + }).collect::>(); - if variants.is_empty() { - return None; - } + let index_types = index_types + .iter() + .map(|(k, types)| { + let idx = LitInt::new(&Literal::isize_unsuffixed(*k).to_string(), Span::call_site()); - let catchall = if variants.len() < count { - quote!{ _ => () } - } else { - quote!() - }; + let docs = types.iter().filter_map(|(_, d)| d.clone()).collect::>(); - Some(quote! { - Some(#idx) => match &mut *this { - #(#variants)* - #catchall - } - }) - }).collect::>(); - - method_registrations.push(quote!{ - methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { - { - let this = this.borrow::()?; - match _idx.as_integer() { - #(#indexes)* - _ => () + let doc = if docs.is_empty() { + quote!(()) + } else { + let docs = docs.join("\n"); + quote!(#docs) + }; + + let mut types = types.iter().map(|(ty, _)| quote!(<#ty as mlua_extras::typed::Typed>::ty())); + let first = types.next().unwrap(); + + quote!(methods.index_as(#idx, #first #(|#types)*, #doc);) + }); + + method_registrations.push(quote!{ + #(#index_types)* + + methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + { + let this = this.borrow::()?; + match _idx.as_integer() { + #(#indexes)* + _ => () + } } - } - let metatable = this.metatable()?; - if let Ok(usr) = metatable.get::("__usr_index") { - return usr.call::((this.clone(), _idx.clone())); - } - - Err(mlua_extras::mlua::Error::runtime(match _idx { - mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), - mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), - _ => "type variant does not contain index".into() - })) - }); + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_index") { + return usr.call::((this.clone(), _idx.clone())); + } + + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), + _ => "type variant does not contain index".into() + })) + }); - methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { - { - let mut this = this.borrow_mut::()?; - match _idx.as_integer() { - #(#new_indexes)* - _ => () + methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { + { + let mut this = this.borrow_mut::()?; + match _idx.as_integer() { + #(#new_indexes)* + _ => () + } } - } - let metatable = this.metatable()?; - if let Ok(usr) = metatable.get::("__usr_newindex") { - return usr.call::>((this.clone(), _idx.clone(), _value)); - } + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_newindex") { + return usr.call::>((this.clone(), _idx.clone(), _value)); + } - Err(mlua_extras::mlua::Error::runtime(match _idx { - mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), - mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), - _ => "type variant does not contain index".into() - })) + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), + _ => "type variant does not contain index".into() + })) + }); }); + } + + let variants = enum_variants.iter().map(|(v, n)| { + let name = n.to_string(); + quote!(Self::#v => #name) }); - } - let variants = enum_variants.iter().map(|(v, n)| { - let name = n.to_string(); - quote!(Self::#v => #name) - }); + let variant_names = enum_variants.iter().map(|(_, n)| n.to_string()); + { + let doc_stmt = self.is_typed().then_some(quote!(fields.document("Full list of variant name");)); + field_registrations.push(quote!{ + #doc_stmt + fields.add_field("_variants", [#(#variant_names,)*]); + }); + } - quote! { - impl #name { - #[doc(hidden)] - fn __auto_add_fields>(fields: &mut F) { + { + let doc_stmt = self.is_typed().then_some(quote!(fields.document("Current variant name");)); + field_registrations.push(quote!{ + #doc_stmt fields.add_field_method_get("_variant", |_lua, this| { Ok(match this { #(#variants,)* }) }); - - #(#field_registrations)* - } - #[doc(hidden)] - fn __implicit_methods>(methods: &mut M) { - #(#method_registrations)* - } + }); } - impl mlua_extras::mlua::UserData for #name { - fn add_fields>(fields: &mut F) { - Self::__auto_add_fields(fields); - } - - fn add_methods>(methods: &mut M) { - Self::__implicit_methods(methods); - - use mlua_extras::__DefaultAutoMethods as _; - Self::__auto_add_methods(methods); - } - } - } -} \ No newline at end of file + (field_registrations, method_registrations) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bc1372a..1dfb207 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub mod ser; + #[cfg(feature="mlua")] pub mod typed; #[cfg(feature="mlua")] @@ -7,7 +9,7 @@ pub mod extras; pub use mlua; #[cfg(feature="macros")] -pub use mlua_extras_derive::{Typed, UserData, user_data_impl}; +pub use mlua_extras_derive::{UserData, user_data_impl, Typed, TypedUserData, typed_user_data_impl}; #[cfg(feature = "send")] /// Used by the `send` feature @@ -27,4 +29,12 @@ pub trait __DefaultAutoMethods: Sized { fn __auto_add_methods(_m: &mut M) {} } #[cfg(feature = "macros")] -impl __DefaultAutoMethods for T {} \ No newline at end of file +impl __DefaultAutoMethods for T {} + +#[cfg(feature = "macros")] +#[doc(hidden)] +pub trait __DefaultAutoFields: Sized { + fn __auto_add_fields(_f: &mut F) {} +} +#[cfg(feature = "macros")] +impl __DefaultAutoFields for T {} diff --git a/src/ser.rs b/src/ser.rs new file mode 100644 index 0000000..7fd7800 --- /dev/null +++ b/src/ser.rs @@ -0,0 +1,658 @@ +use serde::ser::{ + self, Error as _, Serialize, SerializeMap, SerializeSeq, SerializeStruct, Serializer, +}; +use std::fmt::{self, Write}; + +#[derive(Debug)] +pub struct Error(String); + +impl ser::Error for Error { + fn custom(msg: T) -> Self { + Error(msg.to_string()) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for Error {} + +/// Convert any type that is Serializable into it's lua value representation +pub fn to_lua_repr(value: &T) -> Result { + let mut out = String::new(); + value.serialize(&mut LuaSerializer { out: &mut out })?; + Ok(out) +} + +struct LuaSerializer<'a> { + out: &'a mut String, +} + +impl<'a, 'b> Serializer for &'a mut LuaSerializer<'b> { + type Ok = (); + type Error = Error; + + type SerializeSeq = LuaSeq<'a, 'b>; + type SerializeTuple = LuaSeq<'a, 'b>; + type SerializeTupleStruct = LuaSeq<'a, 'b>; + type SerializeTupleVariant = LuaSeq<'a, 'b>; + type SerializeMap = LuaMap<'a, 'b>; + type SerializeStruct = LuaMap<'a, 'b>; + type SerializeStructVariant = LuaMap<'a, 'b>; + + fn serialize_bool(self, v: bool) -> Result<(), Error> { + self.out.push_str(if v { "true" } else { "false" }); + Ok(()) + } + + fn serialize_i8(self, v: i8) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_i16(self, v: i16) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_i32(self, v: i32) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_i64(self, v: i64) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + + fn serialize_u8(self, v: u8) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_u16(self, v: u16) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_u32(self, v: u32) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + fn serialize_u64(self, v: u64) -> Result<(), Error> { + write!(self.out, "{v}").map_err(Error::custom) + } + + fn serialize_f32(self, v: f32) -> Result<(), Error> { + if v.is_nan() { + self.out.push_str("0/0"); + } else if v.is_infinite() { + if v.is_sign_positive() { + self.out.push_str("math.huge"); + } else { + self.out.push_str("-math.huge"); + } + } else { + let mut buf = ryu::Buffer::new(); + let s = buf.format_finite(v); + + if s.contains('.') { + self.out.push_str(s); + } else { + let s = format!("{s}.0"); + self.out.push_str(&s); + } + } + + Ok(()) + } + + fn serialize_f64(self, v: f64) -> Result<(), Error> { + let roundedf32 = v as f32; + if (roundedf32 as f64) == v { + self.serialize_f32(roundedf32)?; + } else { + if v.is_nan() { + self.out.push_str("0/0"); + } else if v.is_infinite() { + if v.is_sign_positive() { + self.out.push_str("math.huge"); + } else { + self.out.push_str("-math.huge"); + } + } else { + let mut buf = ryu::Buffer::new(); + let s = buf.format_finite(v); + + if s.contains('.') { + self.out.push_str(s); + } else { + let s = format!("{s}.0"); + self.out.push_str(&s); + } + } + } + + Ok(()) + } + + fn serialize_char(self, v: char) -> Result<(), Error> { + self.serialize_str(&v.to_string()) + } + + fn serialize_str(self, v: &str) -> Result<(), Error> { + self.out.push('"'); + for ch in v.chars() { + match ch { + '\\' => self.out.push_str("\\\\"), + '"' => self.out.push_str("\\\""), + '\n' => self.out.push_str("\\n"), + '\r' => self.out.push_str("\\r"), + '\t' => self.out.push_str("\\t"), + c => self.out.push(c), + } + } + self.out.push('"'); + Ok(()) + } + + fn serialize_bytes(self, v: &[u8]) -> Result<(), Error> { + let mut seq = self.serialize_seq(Some(v.len()))?; + for b in v { + seq.serialize_element(b)?; + } + seq.end() + } + + fn serialize_none(self) -> Result<(), Error> { + self.out.push_str("nil"); + Ok(()) + } + + fn serialize_some(self, value: &T) -> Result<(), Error> { + value.serialize(self) + } + + fn serialize_unit(self) -> Result<(), Error> { + self.out.push_str("nil"); + Ok(()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Error> { + self.out.push_str("nil"); + Ok(()) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result<(), Error> { + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result<(), Error> { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result<(), Error> { + self.out.push('{'); + write!(self.out, "{} = ", lua_ident_or_bracket(variant)).map_err(Error::custom)?; + value.serialize(&mut *self)?; + self.out.push('}'); + Ok(()) + } + + fn serialize_seq(self, _len: Option) -> Result { + self.out.push('{'); + Ok(LuaSeq { + ser: self, + first: true, + closes_twice: false, + }) + } + + fn serialize_tuple(self, _len: usize) -> Result { + self.serialize_seq(None) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + self.serialize_seq(None) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + self.out.push('{'); + write!(self.out, "{} = ", lua_ident_or_bracket(variant)).map_err(Error::custom)?; + self.out.push('{'); + Ok(LuaSeq { + ser: self, + first: true, + closes_twice: true, + }) + } + + fn serialize_map(self, _len: Option) -> Result { + self.out.push('{'); + Ok(LuaMap { + ser: self, + first: true, + closes_twice: false, + }) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + self.serialize_map(None) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + self.out.push('{'); + write!(self.out, "{} = {{", lua_ident_or_bracket(variant)).map_err(Error::custom)?; + Ok(LuaMap { + ser: self, + first: true, + closes_twice: true, + }) + } +} + +struct LuaSeq<'a, 'b> { + ser: &'a mut LuaSerializer<'b>, + first: bool, + closes_twice: bool, +} + +impl<'a, 'b> LuaSeq<'a, 'b> { + fn comma(&mut self) { + if !self.first { + self.ser.out.push_str(", "); + } + self.first = false; + } +} + +impl<'a, 'b> SerializeSeq for LuaSeq<'a, 'b> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Error> { + self.comma(); + value.serialize(&mut *self.ser) + } + + fn end(self) -> Result<(), Error> { + self.ser.out.push('}'); + if self.closes_twice { + self.ser.out.push('}'); + } + Ok(()) + } +} + +impl<'a, 'b> ser::SerializeTuple for LuaSeq<'a, 'b> { + type Ok = (); + type Error = Error; + fn serialize_element(&mut self, value: &T) -> Result<(), Error> { + SerializeSeq::serialize_element(self, value) + } + fn end(self) -> Result<(), Error> { + SerializeSeq::end(self) + } +} + +impl<'a, 'b> ser::SerializeTupleStruct for LuaSeq<'a, 'b> { + type Ok = (); + type Error = Error; + fn serialize_field(&mut self, value: &T) -> Result<(), Error> { + SerializeSeq::serialize_element(self, value) + } + fn end(self) -> Result<(), Error> { + SerializeSeq::end(self) + } +} + +impl<'a, 'b> ser::SerializeTupleVariant for LuaSeq<'a, 'b> { + type Ok = (); + type Error = Error; + fn serialize_field(&mut self, value: &T) -> Result<(), Error> { + SerializeSeq::serialize_element(self, value) + } + fn end(self) -> Result<(), Error> { + SerializeSeq::end(self) + } +} + +struct LuaMap<'a, 'b> { + ser: &'a mut LuaSerializer<'b>, + first: bool, + closes_twice: bool, +} + +impl<'a, 'b> LuaMap<'a, 'b> { + fn comma(&mut self) { + if !self.first { + self.ser.out.push_str(", "); + } + self.first = false; + } +} + +impl<'a, 'b> SerializeMap for LuaMap<'a, 'b> { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Error> { + self.comma(); + key.serialize(KeySerializer { out: self.ser.out })?; + self.ser.out.push_str(" = "); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Error> { + value.serialize(&mut *self.ser) + } + + fn end(self) -> Result<(), Error> { + self.ser.out.push('}'); + if self.closes_twice { + self.ser.out.push('}'); + } + Ok(()) + } +} + +impl<'a, 'b> SerializeStruct for LuaMap<'a, 'b> { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Error> { + self.comma(); + self.ser.out.push_str(lua_ident_or_bracket(key)); + self.ser.out.push_str(" = "); + value.serialize(&mut *self.ser) + } + + fn end(self) -> Result<(), Error> { + SerializeMap::end(self) + } +} + +impl<'a, 'b> ser::SerializeStructVariant for LuaMap<'a, 'b> { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Error> { + SerializeStruct::serialize_field(self, key, value) + } + + fn end(self) -> Result<(), Error> { + SerializeMap::end(self) + } +} + +struct KeySerializer<'a> { + out: &'a mut String, +} + +impl<'a> Serializer for KeySerializer<'a> { + type Ok = (); + type Error = Error; + + type SerializeSeq = ser::Impossible<(), Error>; + type SerializeTuple = ser::Impossible<(), Error>; + type SerializeTupleStruct = ser::Impossible<(), Error>; + type SerializeTupleVariant = ser::Impossible<(), Error>; + type SerializeMap = ser::Impossible<(), Error>; + type SerializeStruct = ser::Impossible<(), Error>; + type SerializeStructVariant = ser::Impossible<(), Error>; + + fn serialize_str(self, v: &str) -> Result<(), Error> { + self.out.push_str(lua_ident_or_bracket(v)); + Ok(()) + } + + fn serialize_bool(self, v: bool) -> Result<(), Error> { + write!(self.out, "[{}]", if v { "true" } else { "false" }).map_err(Error::custom) + } + + fn serialize_i64(self, v: i64) -> Result<(), Error> { + write!(self.out, "[{v}]").map_err(Error::custom) + } + + fn serialize_u64(self, v: u64) -> Result<(), Error> { + write!(self.out, "[{v}]").map_err(Error::custom) + } + + fn serialize_unit(self) -> Result<(), Error> { + Err(Error::custom("unit cannot be a Lua table key")) + } + + fn serialize_none(self) -> Result<(), Error> { + Err(Error::custom("nil cannot be a Lua table key")) + } + + fn serialize_some(self, value: &T) -> Result<(), Error> { + value.serialize(self) + } + + fn serialize_i8(self, v: i8) -> Result<(), Error> { + self.serialize_i64(v as i64) + } + fn serialize_i16(self, v: i16) -> Result<(), Error> { + self.serialize_i64(v as i64) + } + fn serialize_i32(self, v: i32) -> Result<(), Error> { + self.serialize_i64(v as i64) + } + fn serialize_u8(self, v: u8) -> Result<(), Error> { + self.serialize_u64(v as u64) + } + fn serialize_u16(self, v: u16) -> Result<(), Error> { + self.serialize_u64(v as u64) + } + fn serialize_u32(self, v: u32) -> Result<(), Error> { + self.serialize_u64(v as u64) + } + + fn serialize_f32(self, _v: f32) -> Result<(), Error> { + Err(Error::custom("float keys not supported")) + } + fn serialize_f64(self, _v: f64) -> Result<(), Error> { + Err(Error::custom("float keys not supported")) + } + fn serialize_char(self, v: char) -> Result<(), Error> { + self.serialize_str(&v.to_string()) + } + fn serialize_bytes(self, _v: &[u8]) -> Result<(), Error> { + Err(Error::custom("bytes keys not supported")) + } + fn serialize_unit_struct(self, _: &'static str) -> Result<(), Error> { + self.serialize_unit() + } + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result<(), Error> { + self.serialize_str(variant) + } + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result<(), Error> { + value.serialize(self) + } + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: &T, + ) -> Result<(), Error> { + Err(Error::custom("complex keys not supported")) + } + fn serialize_seq(self, _: Option) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_tuple(self, _: usize) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_map(self, _: Option) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_struct(self, _: &'static str, _: usize) -> Result { + Err(Error::custom("complex keys not supported")) + } + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::custom("complex keys not supported")) + } +} + +fn lua_ident_or_bracket(s: &str) -> &str { + if is_lua_ident(s) { + s + } else { + // This helper returns only &str, so callers that need brackets for + // arbitrary strings should handle that separately if needed. + // For simple struct fields, keep them identifier-safe or rename them. + panic!("non-identifier key: {s}"); + } +} + +fn is_lua_ident(s: &str) -> bool { + let mut chars = s.chars(); + match chars.next() { + Some(c) if c == '_' || c.is_ascii_alphabetic() => {} + _ => return false, + } + chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use serde::Serialize; + + use super::*; + + /// Expect {type} with {value} to serialize to {expected} + /// + /// `expect!(type, &value, expected)` + /// + /// ``` + /// exptect!(u8, &3, "3"); + /// ``` + macro_rules! expect { + ( $t:ty, $value:expr, $expected:literal ) => { + let result = to_lua_repr::<$t>($value).unwrap(); + assert_eq!(result, $expected); + }; + } + + #[test] + fn test_rust_literal_values() { + expect!(u8, &3, "3"); + expect!(u16, &3, "3"); + expect!(u32, &3, "3"); + expect!(u64, &3, "3"); + expect!(i8, &3, "3"); + expect!(i16, &3, "3"); + expect!(i32, &3, "3"); + expect!(i64, &3, "3"); + expect!(f32, &3.0, "3.0"); + expect!(f32, &3.1, "3.1"); + expect!(f64, &3.0, "3.0"); + expect!(f64, &3.1, "3.1"); + + expect!(&str, &"test", "\"test\""); + expect!(String, &"test".to_string(), "\"test\""); + + expect!(bool, &true, "true"); + expect!(bool, &false, "false"); + } + + #[test] + fn test_rust_builtin_types() { + expect!(Option, &None, "nil"); + expect!(Option, &Some(true), "true"); + expect!(&[u8], &b"test".as_slice(), "{116, 101, 115, 116}"); + expect!(BTreeMap<&str, bool>, &BTreeMap::from([("test", false), ("test2", true)]), "{test = false, test2 = true}"); + expect!( + Vec<&str>, + &Vec::from(["test", "test2"]), + "{\"test\", \"test2\"}" + ); + } + + #[derive(Serialize)] + struct Person { + name: String, + age: usize, + } + + #[test] + fn test_rust_custom_types() { + expect!( + Person, + &Person { + name: "Test".into(), + age: 10 + }, + "{name = \"Test\", age = 10}" + ); + } +} diff --git a/src/typed/class/mod.rs b/src/typed/class/mod.rs index db593a8..dac47cb 100644 --- a/src/typed/class/mod.rs +++ b/src/typed/class/mod.rs @@ -10,7 +10,7 @@ mod wrapped; mod standard; pub use wrapped::WrappedBuilder; -pub use standard::TypedClassBuilder; +pub use standard::{TypedClassBuilder, TypedClass}; /// Typed variant of [`mlua::UserData`] pub trait TypedUserData: Sized { @@ -158,10 +158,10 @@ pub trait TypedDataMethods { fn ret_as(&mut self, ty: impl Into, doc: impl IntoDocComment) -> &mut Self; /// Adds an index field with a type and doc comment to the class definition - fn index(&mut self, idx: usize, doc: impl IntoDocComment) -> &mut Self; + fn index(&mut self, idx: isize, doc: impl IntoDocComment) -> &mut Self; /// Adds an index field with a type and doc comment to the class definition - fn index_as(&mut self, idx: usize, ty: impl Into, doc: impl IntoDocComment) -> &mut Self; + fn index_as(&mut self, idx: isize, ty: impl Into, doc: impl IntoDocComment) -> &mut Self; } /// Typed variant of [`mlua::UserDataFields`] diff --git a/src/typed/class/standard.rs b/src/typed/class/standard.rs index 58e21bc..bade26e 100644 --- a/src/typed/class/standard.rs +++ b/src/typed/class/standard.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::BTreeMap}; use mlua::{AnyUserData, FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, Lua}; use crate::{ - MaybeSend, typed::{Field, Func, Index, IntoDocComment, Type} + MaybeSend, ser::to_lua_repr, typed::{Field, Func, Index, IntoDocComment, StaticField, Type} }; use super::{ @@ -11,20 +11,16 @@ use super::{ TypedUserData, }; -/// Type information for a lua `class`. This happens to be a [`TypedUserData`] #[derive(Default, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -pub struct TypedClassBuilder { +pub struct TypedClass { pub type_doc: Option>, - queued_doc: Option>, - queued_ty: Option, - queued_params: Vec<(Option, String, Option>)>, - queued_returns: Vec<(Option, Option>)>, pub derives: Vec, pub fields: BTreeMap, - pub static_fields: BTreeMap, + pub static_fields: BTreeMap, pub meta_fields: BTreeMap, + pub static_meta_fields: BTreeMap, pub methods: BTreeMap, pub meta_methods: BTreeMap, @@ -32,10 +28,32 @@ pub struct TypedClassBuilder { pub functions: BTreeMap, pub meta_functions: BTreeMap, } +impl TypedClass { + /// Check if any of there are any meta fields, functions, or methods present + pub fn is_meta_empty(&self) -> bool { + self.meta_fields.is_empty() + && self.static_meta_fields.is_empty() + && self.meta_functions.is_empty() + && self.meta_methods.is_empty() + } +} + +/// Type information for a lua `class`. This happens to be a [`TypedUserData`] +#[derive(Default, Debug, Clone)] +pub struct TypedClassBuilder { + lua: Lua, + + queued_doc: Option>, + queued_ty: Option, + queued_params: Vec<(Option, String, Option>)>, + queued_returns: Vec<(Option, Option>)>, + + pub typed_class: TypedClass, +} impl From for Type { fn from(value: TypedClassBuilder) -> Self { - Type::Class(Box::new(value)) + Type::Class(Box::new(value.typed_class)) } } @@ -48,43 +66,40 @@ impl TypedClassBuilder { tcb } + pub fn build(self) -> TypedClass { + self.typed_class + } + /// Skip/Remove a field field from the class definition pub fn skip_field(mut self, idx: impl Into) -> Self { - self.fields.remove(&idx.into()); + self.typed_class.fields.remove(&idx.into()); self } /// Skip/Remove a method from the class definition pub fn skip_method(mut self, idx: impl Into) -> Self { - self.methods.remove(&idx.into()); + self.typed_class.methods.remove(&idx.into()); self } /// Skip/Remove a meta method from the class definition pub fn skip_meta_method(mut self, idx: impl Into) -> Self { - self.meta_methods.remove(&idx.into()); + self.typed_class.meta_methods.remove(&idx.into()); self } /// Skip/Remove a function from the class definition pub fn skip_function(mut self, idx: impl Into) -> Self { - self.functions.remove(&idx.into()); + self.typed_class.functions.remove(&idx.into()); self } /// Skip/Remove a meta function from the class definition pub fn skip_meta_function(mut self, idx: impl Into) -> Self { - self.meta_functions.remove(&idx.into()); + self.typed_class.meta_functions.remove(&idx.into()); self } - /// Check if any of there are any meta fields, functions, or methods present - pub fn is_meta_empty(&self) -> bool { - self.meta_fields.is_empty() - && self.meta_functions.is_empty() - && self.meta_methods.is_empty() - } - /// Creates a new typed field and adds it to the class's type information /// /// # Example @@ -100,7 +115,27 @@ impl TypedClassBuilder { /// .field("message", Type::string(), format!("A message for {NAME}")); /// ``` pub fn field(mut self, key: impl Into, ty: Type, doc: impl IntoDocComment) -> Self { - self.fields.insert(key.into(), Field::new(ty, doc)); + self.typed_class.fields.insert(key.into(), Field::new(ty, doc)); + self + } + + pub fn static_field(mut self, key: impl Into, value: V, doc: impl IntoDocComment) -> Self + where + V: Typed + IntoLua + { + let value = match value.into_lua(&self.lua) { + Ok(value) => to_lua_repr(&value).map_err(mlua::Error::runtime), + Err(err) => Err(err) + }; + + if let Ok(value) = value { + self.typed_class.static_fields.insert(key.into(), StaticField::new(V::ty(), doc, value)); + } + self + } + + pub fn inherit(mut self, parent: &TypedClass) -> Self { + self.typed_class.static_fields.extend(parent.static_fields.clone()); self } @@ -125,7 +160,7 @@ impl TypedClassBuilder { Params: TypedMultiValue, Returns: TypedMultiValue, { - self.functions.insert( + self.typed_class.functions.insert( key.into(), Func::new::( doc, @@ -160,7 +195,7 @@ impl TypedClassBuilder { Params: TypedMultiValue, Returns: TypedMultiValue, { - self.methods.insert( + self.typed_class.methods.insert( key.into(), Func::new::( doc, @@ -186,7 +221,7 @@ impl TypedClassBuilder { /// .meta_field("message", Type::string(), format!("A message for {NAME}")); /// ``` pub fn meta_field(mut self, key: impl Into, ty: Type, doc: impl IntoDocComment) -> Self { - self.meta_fields.insert(key.into(), Field::new(ty, doc)); + self.typed_class.meta_fields.insert(key.into(), Field::new(ty, doc)); self } @@ -211,7 +246,7 @@ impl TypedClassBuilder { Params: TypedMultiValue, Returns: TypedMultiValue, { - self.meta_functions.insert( + self.typed_class.meta_functions.insert( key.into(), Func::new::( doc, @@ -248,7 +283,7 @@ impl TypedClassBuilder { Params: TypedMultiValue, Returns: TypedMultiValue, { - self.meta_methods.insert( + self.typed_class.meta_methods.insert( key.into(), Func::new::( doc, @@ -261,17 +296,17 @@ impl TypedClassBuilder { /// Add a child class that this class derives pub fn derive(mut self, parent: impl std::fmt::Display) -> Self { - self.derives.push(parent.to_string()); + self.typed_class.derives.push(parent.to_string()); self } } impl TypedDataDocumentation for TypedClassBuilder { fn add(&mut self, doc: &str) -> &mut Self { - if let Some(type_doc) = self.type_doc.as_mut() { + if let Some(type_doc) = self.typed_class.type_doc.as_mut() { *type_doc = format!("{type_doc}\n{doc}").into() } else { - self.type_doc = Some(doc.to_string().into()) + self.typed_class.type_doc = Some(doc.to_string().into()) } self } @@ -288,25 +323,30 @@ impl TypedDataFields for TypedClassBuilder { self } - fn add_field(&mut self, name: impl Into, _: V) + fn add_field(&mut self, name: impl Into, value: V) where V: IntoLua + Clone + 'static + Typed, { - let name: Cow<'static, str> = name.into().into(); - let ty = self.queued_ty.take().unwrap_or(V::as_param()); - self.static_fields - .entry(name.into()) - .and_modify({ - let ty = ty.clone(); - |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); - v.ty = v.ty.clone() | ty; - } - }) - .or_insert(Field { - ty, - doc: self.queued_doc.take().map(|v| v.into()), - }); + let value = match value.into_lua(&self.lua) { + Ok(value) => to_lua_repr(&value).map_err(mlua::Error::runtime), + Err(err) => Err(err) + }; + + if let Ok(value) = value { + let name: Cow<'static, str> = name.into().into(); + let ty = self.queued_ty.take().unwrap_or(V::as_param()); + let value: Cow<'static, str> = value.into(); + + self.typed_class.static_fields + .insert( + name.into(), + StaticField::new( + ty, + self.queued_doc.take(), + value, + ) + ); + } } fn add_field_function_set(&mut self, name: S, _: F) @@ -317,12 +357,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(A::as_param()); - self.static_fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -340,12 +385,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(R::as_return()); - self.static_fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -365,12 +415,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(A::as_param() | R::as_return()); - self.static_fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -388,12 +443,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(A::as_param()); - self.fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -411,12 +471,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(R::as_return()); - self.fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -436,12 +501,17 @@ impl TypedDataFields for TypedClassBuilder { { let name: Cow<'static, str> = name.into().into(); let ty = self.queued_ty.take().unwrap_or(A::as_param() | R::as_return()); - self.fields + self.typed_class.fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -451,25 +521,30 @@ impl TypedDataFields for TypedClassBuilder { }); } - fn add_meta_field(&mut self, meta: impl Into, _: V) + fn add_meta_field(&mut self, meta: impl Into, value: V) where V: IntoLua + Typed + 'static, { - let name: Cow<'static, str> = meta.into().into(); - let ty = self.queued_ty.take().unwrap_or(V::as_param()); - self.meta_fields - .entry(name.into()) - .and_modify({ - let ty = ty.clone(); - |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); - v.ty = v.ty.clone() | ty; - } - }) - .or_insert(Field { - ty, - doc: self.queued_doc.take().map(|v| v.into()), - }); + let value = match value.into_lua(&self.lua) { + Ok(value) => to_lua_repr(&value).map_err(mlua::Error::runtime), + Err(err) => Err(err) + }; + + if let Ok(value) = value { + let name: Cow<'static, str> = meta.into().into(); + let ty = self.queued_ty.take().unwrap_or(V::as_param()); + let value: Cow<'static, str> = value.into(); + + self.typed_class.static_meta_fields + .insert( + name.into(), + StaticField::new( + ty, + self.queued_doc.take(), + value, + ) + ); + } } fn add_meta_field_with(&mut self, meta: impl Into, _: F) @@ -479,12 +554,17 @@ impl TypedDataFields for TypedClassBuilder { let name: Cow<'static, str> = meta.into().into(); let ty = self.queued_ty.take().unwrap_or(R::as_return()); - self.meta_fields + self.typed_class.meta_fields .entry(name.into()) .and_modify({ let ty = ty.clone(); |v| { - v.doc = self.queued_doc.take().map(|v| v.into()); + if let Some(doc) = self.queued_doc.take() { + v.doc = Some(match v.doc.take() { + Some(d) => format!("{d}\n{doc}").into(), + None => doc + }); + } v.ty = v.ty.clone() | ty; } }) @@ -524,13 +604,13 @@ impl TypedDataMethods for TypedClassBuilder { self } - fn index(&mut self, idx: usize, doc: impl IntoDocComment) -> &mut Self { - self.fields.insert(idx.into(), Field { ty: I::as_param(), doc: doc.into_doc_comment() }); + fn index(&mut self, idx: isize, doc: impl IntoDocComment) -> &mut Self { + self.typed_class.fields.insert(idx.into(), Field { ty: I::as_param(), doc: doc.into_doc_comment() }); self } - fn index_as(&mut self, idx: usize, ty: impl Into, doc: impl IntoDocComment) -> &mut Self { - self.fields.insert(idx.into(), Field { ty: ty.into(), doc: doc.into_doc_comment() }); + fn index_as(&mut self, idx: isize, ty: impl Into, doc: impl IntoDocComment) -> &mut Self { + self.typed_class.fields.insert(idx.into(), Field { ty: ty.into(), doc: doc.into_doc_comment() }); self } @@ -542,7 +622,7 @@ impl TypedDataMethods for TypedClassBuilder { M: 'static + MaybeSend + Fn(&Lua, &T, A) -> mlua::Result, { let name: Cow<'static, str> = name.into().into(); - self.methods.insert( + self.typed_class.methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -560,7 +640,7 @@ impl TypedDataMethods for TypedClassBuilder { F: 'static + MaybeSend + Fn(&Lua, A) -> mlua::Result, { let name: Cow<'static, str> = name.into().into(); - self.functions.insert( + self.typed_class.functions.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -578,7 +658,7 @@ impl TypedDataMethods for TypedClassBuilder { M: 'static + MaybeSend + FnMut(&Lua, &mut T, A) -> mlua::Result, { let name: Cow<'static, str> = name.into().into(); - self.methods.insert( + self.typed_class.methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -595,7 +675,7 @@ impl TypedDataMethods for TypedClassBuilder { M: 'static + MaybeSend + Fn(&Lua, &T, A) -> mlua::Result, { let name: Cow<'static, str> = meta.into().into(); - self.meta_methods.insert( + self.typed_class.meta_methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -615,7 +695,7 @@ impl TypedDataMethods for TypedClassBuilder { R: IntoLuaMulti + TypedMultiValue, { let name: Cow<'static, str> = name.into().into(); - self.methods.insert( + self.typed_class.methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -635,7 +715,7 @@ impl TypedDataMethods for TypedClassBuilder { R: IntoLuaMulti + TypedMultiValue, { let name: Cow<'static, str> = name.into().into(); - self.methods.insert( + self.typed_class.methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -653,7 +733,7 @@ impl TypedDataMethods for TypedClassBuilder { F: 'static + MaybeSend + FnMut(&Lua, A) -> mlua::Result, { let name: Cow<'static, str> = name.into().into(); - self.functions.insert( + self.typed_class.functions.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -670,7 +750,7 @@ impl TypedDataMethods for TypedClassBuilder { F: 'static + MaybeSend + Fn(&Lua, A) -> mlua::Result, { let name: Cow<'static, str> = meta.into().into(); - self.meta_functions.insert( + self.typed_class.meta_functions.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -690,7 +770,7 @@ impl TypedDataMethods for TypedClassBuilder { FR: 'static + MaybeSend + std::future::Future>, { let name: Cow<'static, str> = name.into().into(); - self.functions.insert( + self.typed_class.functions.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -707,7 +787,7 @@ impl TypedDataMethods for TypedClassBuilder { M: 'static + MaybeSend + FnMut(&Lua, &mut T, A) -> mlua::Result, { let name: Cow<'static, str> = meta.into().into(); - self.meta_methods.insert( + self.typed_class.meta_methods.insert( name.into(), Func::new::( self.queued_doc.take(), @@ -724,7 +804,7 @@ impl TypedDataMethods for TypedClassBuilder { F: 'static + MaybeSend + FnMut(&Lua, A) -> mlua::Result, { let name: Cow<'static, str> = meta.into().into(); - self.meta_functions.insert( + self.typed_class.meta_functions.insert( name.into(), Func::new::( self.queued_doc.take(), diff --git a/src/typed/class/wrapped.rs b/src/typed/class/wrapped.rs index 2ce5bfa..2f4abf8 100644 --- a/src/typed/class/wrapped.rs +++ b/src/typed/class/wrapped.rs @@ -4,7 +4,6 @@ use mlua::{ }; #[cfg(feature = "async")] use mlua::{UserDataRef, UserDataRefMut}; - use crate::{MaybeSend, typed::IntoDocComment}; use super::{Type, Typed, TypedDataFields, TypedDataMethods, TypedMultiValue}; @@ -143,11 +142,11 @@ impl<'ctx, T: UserData, U: UserDataMethods> TypedDataMethods for WrappedBu self } - fn index(&mut self, _idx: usize, _doc: impl IntoDocComment) -> &mut Self { + fn index(&mut self, _idx: isize, _doc: impl IntoDocComment) -> &mut Self { self } - fn index_as(&mut self, _idx: usize, _ty: impl Into, _doc: impl IntoDocComment) -> &mut Self { + fn index_as(&mut self, _idx: isize, _ty: impl Into, _doc: impl IntoDocComment) -> &mut Self { self } diff --git a/src/typed/generator/luau_type_file.rs b/src/typed/generator/luau_type_file.rs index a35c449..778852e 100644 --- a/src/typed/generator/luau_type_file.rs +++ b/src/typed/generator/luau_type_file.rs @@ -128,18 +128,9 @@ impl<'writer> LuauDefinitionWriter<'writer> { } writeln!(buffer)?; - // Static fields - for (name, field) in type_data.static_fields.iter() { - self.write_doc_comments( - &mut buffer, - &[field.doc.as_deref()], - "\t", - )?; - writeln!(buffer, "\t{}: {}", name, self.type_signature(&field.ty)?)?; - } - // Instance fields for (name, field) in type_data.fields.iter() { + if name.is_int() { continue; } self.write_doc_comments( &mut buffer, &[field.doc.as_deref()], @@ -243,9 +234,19 @@ impl<'writer> LuauDefinitionWriter<'writer> { )?; } - if !static_fns.is_empty() { + if !static_fns.is_empty() || !type_data.static_fields.is_empty() { writeln!(buffer)?; writeln!(buffer, "declare {}: {{", definition.name)?; + + for (name, field) in type_data.static_fields.iter() { + self.write_doc_comments( + &mut buffer, + &[field.inner.doc.as_deref()], + "\t", + )?; + writeln!(buffer, "\t{}: {},", name, self.type_signature(&field.inner.ty)?)?; + } + for (name, _func) in &static_fns { writeln!( buffer, @@ -345,10 +346,17 @@ impl<'writer> LuauDefinitionWriter<'writer> { } }, Type::Single(value) => { - // Luau recognizes `integer` as a type, but numeric literals - // are inferred as `number` and the two are mutually incompatible, - // making `integer` unusable in practice. Emit `number` instead. - if value == "integer" { "number".to_string() } else { value.to_string() } + match value.as_ref() { + // Luau has `userdata` but it isn't an actual type but instead + // defined luau class types. This will erase the generic userdata + // types to `any`. + "userdata" | "lightuserdata" => "any".into(), + // Luau recognizes `integer` as a type, but numeric literals + // are inferred as `number` and the two are mutually incompatible, + // making `integer` unusable in practice. Emit `number` instead. + "integer" => "number".to_string(), + other => other.to_string(), + } } Type::Tuple(types) => { // Luau doesn't support integer literal keys in table types. @@ -467,7 +475,7 @@ impl<'writer> LuauDefinitionWriter<'writer> { ) -> mlua::Result<()> { for doc in docs.iter().filter_map(|v| *v) { for line in doc.split('\n') { - writeln!(buffer, "{indent}--- {line}")?; + writeln!(buffer, "{indent}-- {line}")?; } } Ok(()) @@ -480,7 +488,7 @@ impl<'writer> LuauDefinitionWriter<'writer> { indent: &str, ) -> mlua::Result<()> { for (i, p) in params.iter().enumerate().filter(|(_, p)| p.doc.is_some()) { - write!(buffer, "{indent}--- @param {} {}", + write!(buffer, "{indent}-- @param {} {}", p.name.as_deref().map(|v| v.to_string()).unwrap_or_else(|| format!("param{}", i + 1)), self.type_signature(&p.ty)? )?; @@ -500,7 +508,7 @@ impl<'writer> LuauDefinitionWriter<'writer> { indent: &str, ) -> mlua::Result<()> { for (i, r) in returns.iter().enumerate().filter(|(_, r)| r.doc.is_some()) { - write!(buffer, "{indent}--- @return {}", self.type_signature(&r.ty)?)?; + write!(buffer, "{indent}-- @return {}", self.type_signature(&r.ty)?)?; if let Some(doc) = r.doc.as_deref() { let doc = doc.replace('\n', ""); write!(buffer, " -- #{} {doc}", i + 1)?; diff --git a/src/typed/generator/luau_type_file_tests.rs b/src/typed/generator/luau_type_file_tests.rs index 458788f..c6e48d9 100644 --- a/src/typed/generator/luau_type_file_tests.rs +++ b/src/typed/generator/luau_type_file_tests.rs @@ -2,9 +2,9 @@ #![cfg(feature = "luau")] use crate::typed::{ - generator::{Definition, DefinitionBuilder, Definitions, Entry, LuauDefinitionFileGenerator}, - function::Return, Field, Func, Index, Param, Type, TypedClassBuilder, + function::Return, + generator::{Definition, DefinitionBuilder, Definitions, Entry, LuauDefinitionFileGenerator}, }; /// Write definitions to a string buffer and return the output. @@ -23,8 +23,15 @@ fn single(def: DefinitionBuilder) -> Definitions { } /// Helper: add a typed value entry to a DefinitionBuilder. -fn with_value(mut builder: DefinitionBuilder, name: &str, ty: Type, doc: Option<&str>) -> DefinitionBuilder { - builder.entries.push(Entry::new_with(name, Type::Value(Box::new(ty)), doc)); +fn with_value( + mut builder: DefinitionBuilder, + name: &str, + ty: Type, + doc: Option<&str>, +) -> DefinitionBuilder { + builder + .entries + .push(Entry::new_with(name, Type::Value(Box::new(ty)), doc)); builder } @@ -95,17 +102,15 @@ fn validate_with_luau_lsp(defs_content: &str, script: &str) { #[test] fn test_enum_type() { - let out = generate(single( - Definition::start().register_as( - "Direction", - Type::r#enum([ - Type::literal("Up"), - Type::literal("Down"), - Type::literal("Left"), - Type::literal("Right"), - ]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Direction", + Type::r#enum([ + Type::literal("Up"), + Type::literal("Down"), + Type::literal("Left"), + Type::literal("Right"), + ]), + ))); assert_eq!( out.trim(), r#"export type Direction = "Up" | "Down" | "Left" | "Right""# @@ -122,17 +127,15 @@ fn test_alias_type() { #[test] fn test_declare_value() { - let out = generate(single( - with_value( - Definition::start(), - "myGlobal", - Type::string(), - Some("A global"), - ), - )); + let out = generate(single(with_value( + Definition::start(), + "myGlobal", + Type::string(), + Some("A global"), + ))); assert_eq!( out.trim(), - "--- A global + "-- A global declare myGlobal: string" ); } @@ -147,8 +150,8 @@ fn test_declare_function() { )); assert_eq!( out.trim(), - "--- @param name string -- The name ---- @return string -- #1 Formatted greeting + "-- @param name string -- The name +-- @return string -- #1 Formatted greeting declare function greet(name: string): string" ); } @@ -156,20 +159,15 @@ declare function greet(name: string): string" #[test] fn test_function_no_return() { let out = generate(single( - Definition::start() - .function::<(String,), ()>("log", ()), + Definition::start().function::<(String,), ()>("log", ()), )); - assert_eq!( - out.trim(), - "declare function log(param1: string): ()" - ); + assert_eq!(out.trim(), "declare function log(param1: string): ()"); } #[test] fn test_function_multi_return() { let out = generate(single( - Definition::start() - .function::("parse", ()), + Definition::start().function::("parse", ()), )); assert_eq!( out.trim(), @@ -185,14 +183,15 @@ fn test_class_with_fields() { Type::class( TypedClassBuilder::default() .field("name", Type::string(), "Player name") - .field("score", Type::integer(), ()), + .field("score", Type::integer(), ()) + .build(), ), ), )); assert_eq!( out.trim(), "declare class Player -\t--- Player name +\t-- Player name \tname: string \tscore: number end" @@ -208,7 +207,8 @@ fn test_class_with_methods() { TypedClassBuilder::default() .field("value", Type::integer(), ()) .method::<(), i64>("getValue", "Get the current value") - .method::<(i64,), ()>("add", ()), + .method::<(i64,), ()>("add", ()) + .build(), ), ), )); @@ -217,7 +217,7 @@ fn test_class_with_methods() { "declare class Counter \tvalue: number \tfunction add(self, param1: number): () -\t--- Get the current value +\t-- Get the current value \tfunction getValue(self): number end" ); @@ -230,7 +230,8 @@ fn test_class_with_functions_separate_table() { "Utils", Type::class( TypedClassBuilder::default() - .function::("upper", ()), + .function::("upper", ()) + .build(), ), ), )); @@ -256,7 +257,8 @@ fn test_class_with_meta_method() { Type::class( TypedClassBuilder::default() .field("x", Type::number(), ()) - .meta_method::<(), String>("__tostring", ()), + .meta_method::<(), String>("__tostring", ()) + .build(), ), ), )); @@ -272,15 +274,17 @@ end" #[test] fn test_class_with_meta_field() { let mut builder = TypedClassBuilder::default(); - builder.meta_fields.insert( + builder.typed_class.meta_fields.insert( Index::from("__count"), Field::new(Type::integer(), "Meta field"), ); - let out = generate(single(Definition::start().register_as("Tracked", Type::class(builder)))); + let out = generate(single( + Definition::start().register_as("Tracked", Type::class(builder.build())), + )); assert_eq!( out.trim(), "declare class Tracked -\t--- Meta field +\t-- Meta field \t__count: number end" ); @@ -289,8 +293,7 @@ end" #[test] fn test_optional_type_sugar() { let out = generate(single( - Definition::start() - .register_as("MaybeStr", Type::string() | Type::nil()), + Definition::start().register_as("MaybeStr", Type::string() | Type::nil()), )); assert_eq!(out.trim(), "export type MaybeStr = string?"); } @@ -298,8 +301,7 @@ fn test_optional_type_sugar() { #[test] fn test_array_type() { let out = generate(single( - Definition::start() - .register_as("Names", Type::array(Type::string())), + Definition::start().register_as("Names", Type::array(Type::string())), )); assert_eq!(out.trim(), "export type Names = { string }"); } @@ -307,25 +309,20 @@ fn test_array_type() { #[test] fn test_map_type() { let out = generate(single( - Definition::start().register_as( - "Scores", - Type::map(Type::string(), Type::integer()), - ), + Definition::start().register_as("Scores", Type::map(Type::string(), Type::integer())), )); assert_eq!(out.trim(), "export type Scores = { [string]: number }"); } #[test] fn test_table_type() { - let out = generate(single( - Definition::start().register_as( - "Config", - Type::table([ - (Index::from("host"), Type::string()), - (Index::from("port"), Type::integer()), - ]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Config", + Type::table([ + (Index::from("host"), Type::string()), + (Index::from("port"), Type::integer()), + ]), + ))); assert_eq!( out.trim(), "export type Config = { host: string, port: number }" @@ -348,31 +345,23 @@ fn test_function_type_signature() { let out = generate(single( Definition::start().register_as("Predicate", func_type), )); - assert_eq!( - out.trim(), - "export type Predicate = (x: number) -> boolean" - ); + assert_eq!(out.trim(), "export type Predicate = (x: number) -> boolean"); } #[test] fn test_tuple_homogeneous() { let out = generate(single( - Definition::start().register_as( - "Pair", - Type::tuple([Type::integer(), Type::integer()]), - ), + Definition::start().register_as("Pair", Type::tuple([Type::integer(), Type::integer()])), )); assert_eq!(out.trim(), "export type Pair = { number }"); } #[test] fn test_tuple_heterogeneous() { - let out = generate(single( - Definition::start().register_as( - "Mixed", - Type::tuple([Type::string(), Type::integer(), Type::boolean()]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Mixed", + Type::tuple([Type::string(), Type::integer(), Type::boolean()]), + ))); assert_eq!( out.trim(), "export type Mixed = { string | number | boolean }" @@ -382,15 +371,10 @@ fn test_tuple_heterogeneous() { #[test] fn test_union_type() { let out = generate(single( - Definition::start().register_as( - "Multi", - Type::string() | Type::integer() | Type::boolean(), - ), + Definition::start() + .register_as("Multi", Type::string() | Type::integer() | Type::boolean()), )); - assert_eq!( - out.trim(), - "export type Multi = string | number | boolean" - ); + assert_eq!(out.trim(), "export type Multi = string | number | boolean"); } #[test] @@ -402,8 +386,8 @@ fn test_doc_comments() { )); assert_eq!( out.trim(), - "--- Greet someone ---- This is multiline + "-- Greet someone +-- This is multiline declare function greet(param1: string): ()" ); } @@ -411,15 +395,19 @@ declare function greet(param1: string): ()" #[test] fn test_class_doc_comment() { let mut builder = TypedClassBuilder::default(); - builder.type_doc = Some("A documented class".into()); + builder.typed_class.type_doc = Some("A documented class".into()); // register_as uses Entry::new (no doc), so set doc on the entry directly let mut def_builder = Definition::start(); - def_builder.entries.push(Entry::new_with("Documented", Type::class(builder), Some("Top-level doc"))); + def_builder.entries.push(Entry::new_with( + "Documented", + Type::class(builder.build()), + Some("Top-level doc"), + )); let out = generate(single(def_builder)); assert_eq!( out.trim(), - "--- Top-level doc ---- A documented class + "-- Top-level doc +-- A documented class declare class Documented end" ); @@ -432,14 +420,12 @@ fn test_enum_referenced_in_value() { Type::literal("Green"), Type::literal("Blue"), ]); - let out = generate(single( - with_value( - Definition::start().register_as("Color", color_enum), - "defaultColor", - Type::named("Color"), - None, - ), - )); + let out = generate(single(with_value( + Definition::start().register_as("Color", color_enum), + "defaultColor", + Type::named("Color"), + None, + ))); assert_eq!( out.trim(), "export type Color = \"Red\" | \"Green\" | \"Blue\" @@ -474,20 +460,15 @@ fn test_extension_custom() { #[test] fn test_luau_lsp_enum_and_value() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Direction", - Type::r#enum([ - Type::literal("Up"), - Type::literal("Down"), - ]), - ), - "dir", - Type::named("Direction"), - None, + let out = generate(single(with_value( + Definition::start().register_as( + "Direction", + Type::r#enum([Type::literal("Up"), Type::literal("Down")]), ), - )); + "dir", + Type::named("Direction"), + None, + ))); validate_with_luau_lsp( &out, r#" @@ -500,8 +481,7 @@ local _u: Direction = "Up" #[test] fn test_luau_lsp_alias() { let out = generate(single( - Definition::start() - .register_as("StringOrNum", Type::string() | Type::number()), + Definition::start().register_as("StringOrNum", Type::string() | Type::number()), )); validate_with_luau_lsp( &out, @@ -529,22 +509,21 @@ local _result: string = greet("world") #[test] fn test_luau_lsp_class_fields_and_methods() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Player", - Type::class( - TypedClassBuilder::default() - .field("name", Type::string(), ()) - .field("score", Type::integer(), ()) - .method::<(), String>("getName", ()), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Player", + Type::class( + TypedClassBuilder::default() + .field("name", Type::string(), ()) + .field("score", Type::integer(), ()) + .method::<(), String>("getName", ()) + .build(), ), - "player", - Type::named("Player"), - None, ), - )); + "player", + Type::named("Player"), + None, + ))); validate_with_luau_lsp( &out, r#" @@ -557,21 +536,20 @@ local _gn: string = player:getName() #[test] fn test_luau_lsp_class_with_meta_method() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Obj", - Type::class( - TypedClassBuilder::default() - .field("x", Type::number(), ()) - .meta_method::<(), String>("__tostring", ()), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Obj", + Type::class( + TypedClassBuilder::default() + .field("x", Type::number(), ()) + .meta_method::<(), String>("__tostring", ()) + .build(), ), - "obj", - Type::named("Obj"), - None, ), - )); + "obj", + Type::named("Obj"), + None, + ))); validate_with_luau_lsp( &out, r#" @@ -583,20 +561,19 @@ local _x: number = obj.x #[test] fn test_luau_lsp_optional_type() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Container", - Type::class( - TypedClassBuilder::default() - .field("value", Type::string() | Type::nil(), ()), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Container", + Type::class( + TypedClassBuilder::default() + .field("value", Type::string() | Type::nil(), ()) + .build(), ), - "c", - Type::named("Container"), - None, ), - )); + "c", + Type::named("Container"), + None, + ))); assert_eq!( out.trim(), "declare class Container @@ -615,20 +592,19 @@ local _v: string? = c.value #[test] fn test_luau_lsp_array_type() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Holder", - Type::class( - TypedClassBuilder::default() - .field("items", Type::array(Type::string()), ()), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Holder", + Type::class( + TypedClassBuilder::default() + .field("items", Type::array(Type::string()), ()) + .build(), ), - "h", - Type::named("Holder"), - None, ), - )); + "h", + Type::named("Holder"), + None, + ))); validate_with_luau_lsp( &out, r#" @@ -639,20 +615,19 @@ local _items: {string} = h.items #[test] fn test_luau_lsp_map_type() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Registry", - Type::class( - TypedClassBuilder::default() - .field("data", Type::map(Type::string(), Type::number()), ()), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Registry", + Type::class( + TypedClassBuilder::default() + .field("data", Type::map(Type::string(), Type::number()), ()) + .build(), ), - "reg", - Type::named("Registry"), - None, ), - )); + "reg", + Type::named("Registry"), + None, + ))); validate_with_luau_lsp( &out, r#" @@ -668,17 +643,11 @@ fn test_luau_lsp_complex_definition() { Definition::start() .register_as( "System", - Type::r#enum([ - Type::literal("Black"), - Type::literal("White"), - ]), + Type::r#enum([Type::literal("Black"), Type::literal("White")]), ) .register_as( "Color", - Type::r#enum([ - Type::named("System"), - Type::integer(), - ]), + Type::r#enum([Type::named("System"), Type::integer()]), ) .register_as( "Example", @@ -686,7 +655,8 @@ fn test_luau_lsp_complex_definition() { TypedClassBuilder::default() .field("color", Type::named("Color"), ()) .method::<(), String>("describe", ()) - .meta_method::<(), String>("__tostring", ()), + .meta_method::<(), String>("__tostring", ()) + .build(), ), ), "example", @@ -730,8 +700,7 @@ fn test_mismatch_variadic_erases_to_any() { // When used as a function parameter, the generated output uses `any` let out = generate(single( - Definition::start() - .function::<(String, Variadic), ()>("log", ()), + Definition::start().function::<(String, Variadic), ()>("log", ()), )); assert_eq!( out.trim(), @@ -746,22 +715,27 @@ fn test_luau_lsp_static_functions() { let mut builder = TypedClassBuilder::default() .field("name", Type::string(), ()) .method::<(), String>("getName", ()); - builder.functions.insert( + builder.typed_class.functions.insert( "create".into(), Func { - params: vec![Param { name: Some("name".into()), ty: Type::string(), doc: None }], - returns: vec![Return { ty: Type::named("Player"), doc: None }], + params: vec![Param { + name: Some("name".into()), + ty: Type::string(), + doc: None, + }], + returns: vec![Return { + ty: Type::named("Player"), + doc: None, + }], doc: None, }, ); - let out = generate(single( - with_value( - Definition::start().register_as("Player", Type::class(builder)), - "player", - Type::named("Player"), - None, - ), - )); + let out = generate(single(with_value( + Definition::start().register_as("Player", Type::class(builder.build())), + "player", + Type::named("Player"), + None, + ))); validate_with_luau_lsp( &out, "local p: Player = Player.create(\"alice\")\nlocal _n: string = p:getName()\nlocal _name: string = p.name\n", @@ -778,7 +752,8 @@ fn test_static_functions_separate_table() { "Factory", Type::class( TypedClassBuilder::default() - .function::("create", "A static factory method"), + .function::("create", "A static factory method") + .build(), ), ), )); @@ -787,7 +762,7 @@ fn test_static_functions_separate_table() { "declare class Factory end ---- A static factory method +-- A static factory method declare function Factory_create(param1: string): number declare Factory: { @@ -803,12 +778,10 @@ declare Factory: { /// which position. #[test] fn test_mismatch_heterogeneous_tuple_loses_position() { - let out = generate(single( - Definition::start().register_as( - "Record", - Type::tuple([Type::string(), Type::integer(), Type::boolean()]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Record", + Type::tuple([Type::string(), Type::integer(), Type::boolean()]), + ))); // Instead of something like [string, integer, boolean], we get // an array whose element type is the union of all tuple types assert_eq!( @@ -824,27 +797,26 @@ fn test_mismatch_heterogeneous_tuple_loses_position() { /// Luau generator maps `Type::integer()` to `number` to avoid this. #[test] fn test_integer_maps_to_number() { - let out = generate(single( - with_value( - Definition::start().register_as( - "Stats", - Type::class( - TypedClassBuilder::default() - .field("count", Type::integer(), "An integer field") - .field("ratio", Type::number(), "A float field"), - ), + let out = generate(single(with_value( + Definition::start().register_as( + "Stats", + Type::class( + TypedClassBuilder::default() + .field("count", Type::integer(), "An integer field") + .field("ratio", Type::number(), "A float field") + .build(), ), - "stats", - Type::named("Stats"), - None, ), - )); + "stats", + Type::named("Stats"), + None, + ))); assert_eq!( out.trim(), "declare class Stats -\t--- An integer field +\t-- An integer field \tcount: number -\t--- A float field +\t-- A float field \tratio: number end @@ -869,27 +841,26 @@ local _r: number = stats.ratio /// assignable to `integer`. #[test] fn test_luau_lsp_integer_fields_accept_numeric_literals() { - let out = generate(single( - with_value( - Definition::start() - .register_as( - "Inventory", - Type::class( - TypedClassBuilder::default() - .field("count", Type::integer(), ()) - .field("weight", Type::number(), ()) - .method::<(i32,), ()>("addItems", ()) - .method::<(), i64>("total", ()), - ), - ) - .param("a", "") - .param("b", "") - .function::<(i32, i32), i32>("add", ()), - "inv", - Type::named("Inventory"), - None, - ), - )); + let out = generate(single(with_value( + Definition::start() + .register_as( + "Inventory", + Type::class( + TypedClassBuilder::default() + .field("count", Type::integer(), ()) + .field("weight", Type::number(), ()) + .method::<(i32,), ()>("addItems", ()) + .method::<(), i64>("total", ()) + .build(), + ), + ) + .param("a", "") + .param("b", "") + .function::<(i32, i32), i32>("add", ()), + "inv", + Type::named("Inventory"), + None, + ))); // The generated output should use `number` everywhere, not `integer` assert!( @@ -921,16 +892,14 @@ local _t: number = inv:total() fn test_mismatch_enum_tuple_variants_flatten() { // An enum where one variant carries a tuple of (integer, string) // and another carries just a string - let out = generate(single( - Definition::start().register_as( - "Payload", - Type::r#enum([ - Type::literal("None"), - Type::tuple([Type::integer(), Type::string()]), - Type::tuple([Type::boolean()]), - ]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Payload", + Type::r#enum([ + Type::literal("None"), + Type::tuple([Type::integer(), Type::string()]), + Type::tuple([Type::boolean()]), + ]), + ))); // The tuple variants become union-arrays, losing arity info assert_eq!( out.trim(), diff --git a/src/typed/generator/type_file.rs b/src/typed/generator/type_file.rs index dfae78d..0349d85 100644 --- a/src/typed/generator/type_file.rs +++ b/src/typed/generator/type_file.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, collections::HashMap, path::Path, slice::Iter}; -use crate::typed::{function::Return, Param, Type}; +use crate::typed::{Field, Param, StaticField, Type, function::Return}; use super::{Definition, Definitions}; @@ -127,17 +127,6 @@ impl<'writer> DefinitionWriter<'writer> { } writeln!(buffer)?; - for (name, field) in type_data.static_fields.iter() { - if let Some(docs) = self.accumulate_docs(&[field.doc.as_deref()]) { - writeln!(buffer, "{}", docs.join("\n"))?; - } - writeln!( - buffer, - "--- @field {name} {}", - self.type_signature(&field.ty)? - )?; - } - for (name, field) in type_data.fields.iter() { if let Some(docs) = self.accumulate_docs(&[field.doc.as_deref()]) { writeln!(buffer, "{}", docs.join("\n"))?; @@ -149,32 +138,39 @@ impl<'writer> DefinitionWriter<'writer> { )?; } - if !type_data.functions.is_empty() || !type_data.methods.is_empty() || !type_data.is_meta_empty() { + if !type_data.static_fields.is_empty() || !type_data.functions.is_empty() || !type_data.methods.is_empty() || !type_data.is_meta_empty() { writeln!(buffer, "local _CLASS_{}_ = {{", definition.name)?; + for (name, StaticField { inner: Field { doc, .. }, default }) in type_data.static_fields.iter() { + if let Some(docs) = self.accumulate_docs(&[doc.as_deref()]) { + writeln!(buffer, "\t{}", docs.join("\n\t"))?; + } + writeln!(buffer, "\t{name} = {default},")?; + } + for (name, func) in type_data.functions.iter() { if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { - writeln!(buffer, " {}", docs.join("\n "))?; + writeln!(buffer, "\t{}", docs.join("\n\t"))?; } writeln!( buffer, - " {},", + "\t{},", self.function_signature( name, &func.params, &func.returns, true )? - .join("\n ") + .join("\n\t") )?; } for (name, func) in type_data.methods.iter() { if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { - writeln!(buffer, " {}", docs.join("\n "))?; + writeln!(buffer, "\t{}", docs.join("\n\t"))?; } writeln!( buffer, - " {},", + "\t{},", self.method_signature( name, definition.name.to_string(), @@ -182,61 +178,64 @@ impl<'writer> DefinitionWriter<'writer> { &func.returns, true )? - .join("\n ") + .join("\n\t") )?; } if !type_data.is_meta_empty() { - if !type_data.meta_fields.is_empty() - || !type_data.meta_functions.is_empty() - || !type_data.meta_methods.is_empty() - { - writeln!(buffer, " __metatable = {{")?; - for (name, field) in type_data.meta_fields.iter() { - if let Some(docs) = self.accumulate_docs(&[field.doc.as_deref()]) { - writeln!(buffer, " {}", docs.join("\n "))?; - } - writeln!(buffer, " --- @type {}", self.type_signature(&field.ty)?)?; - writeln!(buffer, " {name} = nil,")?; + writeln!(buffer, "\t__metatable = {{")?; + for (name, StaticField { inner: Field { ty, doc }, default }) in type_data.static_meta_fields.iter() { + if let Some(docs) = self.accumulate_docs(&[doc.as_deref()]) { + writeln!(buffer, "\t\t{}", docs.join("\n\t\t"))?; } - for (name, func) in type_data.meta_functions.iter() { - if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { - writeln!(buffer, " {}", docs.join("\n "))?; - } - writeln!( - buffer, - " {},", - self.function_signature( - name, - &func.params, - &func.returns, - true - )? - .join("\n ") - )?; + writeln!(buffer, "\t\t--- @type {}", self.type_signature(ty)?)?; + writeln!(buffer, "\t\t{name} = {default},")?; + } + + for (name, field) in type_data.meta_fields.iter() { + if let Some(docs) = self.accumulate_docs(&[field.doc.as_deref()]) { + writeln!(buffer, "\t\t{}", docs.join("\n\t\t"))?; } + writeln!(buffer, "\t\t--- @type {}", self.type_signature(&field.ty)?)?; + writeln!(buffer, "\t\t{name} = nil,")?; + } - for (name, func) in type_data.meta_methods.iter() { - if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { - writeln!(buffer, " {}", docs.join("\n "))?; - } - writeln!( - buffer, - " {},", - self.method_signature( - name, - definition.name.to_string(), - &func.params, - &func.returns, - true - )? - .join("\n ") - )?; + for (name, func) in type_data.meta_functions.iter() { + if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { + writeln!(buffer, "\t\t{}", docs.join("\n\t\t"))?; } - writeln!(buffer, " }}")?; + writeln!( + buffer, + "\t\t{},", + self.function_signature( + name, + &func.params, + &func.returns, + true + )? + .join("\n\t\t") + )?; } + for (name, func) in type_data.meta_methods.iter() { + if let Some(docs) = self.accumulate_docs(&[func.doc.as_deref()]) { + writeln!(buffer, "\t\t{}", docs.join("\n\t\t"))?; + } + writeln!( + buffer, + "\t\t{},", + self.method_signature( + name, + definition.name.to_string(), + &func.params, + &func.returns, + true + )? + .join("\n\t\t") + )?; + } + writeln!(buffer, "\t}}")?; } writeln!(buffer, "}}")?; } diff --git a/src/typed/generator/type_file_tests.rs b/src/typed/generator/type_file_tests.rs index 258f8b2..eb38604 100644 --- a/src/typed/generator/type_file_tests.rs +++ b/src/typed/generator/type_file_tests.rs @@ -1,9 +1,9 @@ #![cfg(test)] use crate::typed::{ - generator::{Definition, DefinitionBuilder, Definitions, Entry, DefinitionFileGenerator}, - function::Return, Index, Param, Type, TypedClassBuilder, + function::Return, + generator::{Definition, DefinitionBuilder, DefinitionFileGenerator, Definitions, Entry}, }; /// Write definitions to a string buffer and return the output. @@ -22,8 +22,15 @@ fn single(def: impl Into) -> Definitions { } /// Helper: add a typed value entry to a DefinitionBuilder. -fn with_value(mut builder: DefinitionBuilder, name: &str, ty: Type, doc: Option<&str>) -> DefinitionBuilder { - builder.entries.push(Entry::new_with(name, Type::Value(Box::new(ty)), doc)); +fn with_value( + mut builder: DefinitionBuilder, + name: &str, + ty: Type, + doc: Option<&str>, +) -> DefinitionBuilder { + builder + .entries + .push(Entry::new_with(name, Type::Value(Box::new(ty)), doc)); builder } @@ -138,17 +145,15 @@ fn test_enum_single_variant() { #[test] fn test_enum_multiple_variants() { - let out = generate(single( - Definition::start().register_as( - "Direction", - Type::r#enum([ - Type::literal("Up"), - Type::literal("Down"), - Type::literal("Left"), - Type::literal("Right"), - ]), - ), - )); + let out = generate(single(Definition::start().register_as( + "Direction", + Type::r#enum([ + Type::literal("Up"), + Type::literal("Down"), + Type::literal("Left"), + Type::literal("Right"), + ]), + ))); assert_eq!( out.trim(), r#"--- @meta @@ -250,7 +255,11 @@ fn test_value_named_class_type() { let out = generate(single(with_value( Definition::start().register_as( "Player", - Type::class(TypedClassBuilder::default().field("name", Type::string(), ())), + Type::class( + TypedClassBuilder::default() + .field("name", Type::string(), ()) + .build(), + ), ), "player", Type::named("Player"), @@ -383,7 +392,7 @@ function ["some.name"]() end"# #[test] fn test_class_empty() { let out = generate(single( - Definition::start().register_as("Empty", Type::class(TypedClassBuilder::default())), + Definition::start().register_as("Empty", Type::class(TypedClassBuilder::default().build())), )); assert_eq!( out.trim(), @@ -401,7 +410,8 @@ fn test_class_with_fields() { Type::class( TypedClassBuilder::default() .field("name", Type::string(), ()) - .field("score", Type::integer(), ()), + .field("score", Type::integer(), ()) + .build(), ), ), )); @@ -422,7 +432,8 @@ fn test_class_field_with_doc() { "Player", Type::class( TypedClassBuilder::default() - .field("name", Type::string(), "The player's name"), + .field("name", Type::string(), "The player's name") + .build(), ), ), )); @@ -439,11 +450,11 @@ fn test_class_field_with_doc() { #[test] fn test_class_doc_comments() { let mut builder = TypedClassBuilder::default(); - builder.type_doc = Some("A class-level doc".into()); + builder.typed_class.type_doc = Some("A class-level doc".into()); let mut def_builder = Definition::start(); def_builder.entries.push(Entry::new_with( "Documented", - Type::class(builder), + Type::class(builder.build()), Some("Top-level doc"), )); let out = generate(single(def_builder)); @@ -464,7 +475,8 @@ fn test_class_with_method_no_extra_params() { "Foo", Type::class( TypedClassBuilder::default() - .method::<(), String>("getValue", "Get the value"), + .method::<(), String>("getValue", "Get the value") + .build(), ), ), )); @@ -474,10 +486,10 @@ fn test_class_with_method_no_extra_params() { --- @class Foo local _CLASS_Foo_ = { - --- Get the value - --- @param self Foo - --- @return string - getValue = function(self) end, +\t--- Get the value +\t--- @param self Foo +\t--- @return string +\tgetValue = function(self) end, }" ); } @@ -489,7 +501,8 @@ fn test_class_with_method_with_params() { "Counter", Type::class( TypedClassBuilder::default() - .method::<(i64,), ()>("add", ()), + .method::<(i64,), ()>("add", ()) + .build(), ), ), )); @@ -499,9 +512,9 @@ fn test_class_with_method_with_params() { --- @class Counter local _CLASS_Counter_ = { - --- @param self Counter - --- @param param1 integer - add = function(self, param1) end, +\t--- @param self Counter +\t--- @param param1 integer +\tadd = function(self, param1) end, }" ); } @@ -513,7 +526,8 @@ fn test_class_with_static_function() { "Utils", Type::class( TypedClassBuilder::default() - .function::("create", "A factory"), + .function::("create", "A factory") + .build(), ), ), )); @@ -523,10 +537,10 @@ fn test_class_with_static_function() { --- @class Utils local _CLASS_Utils_ = { - --- A factory - --- @param param1 string - --- @return integer - create = function(param1) end, +\t--- A factory +\t--- @param param1 string +\t--- @return integer +\tcreate = function(param1) end, }" ); } @@ -539,7 +553,8 @@ fn test_class_fields_and_methods_combined() { Type::class( TypedClassBuilder::default() .field("name", Type::string(), ()) - .method::<(), String>("getName", ()), + .method::<(), String>("getName", ()) + .build(), ), ), )); @@ -550,9 +565,9 @@ fn test_class_fields_and_methods_combined() { --- @class Player --- @field name string local _CLASS_Player_ = { - --- @param self Player - --- @return string - getName = function(self) end, +\t--- @param self Player +\t--- @return string +\tgetName = function(self) end, }" ); } @@ -564,7 +579,8 @@ fn test_class_with_meta_field() { "Tracked", Type::class( TypedClassBuilder::default() - .meta_field("__count", Type::integer(), "Meta count"), + .meta_field("__count", Type::integer(), "Meta count") + .build(), ), ), )); @@ -574,11 +590,11 @@ fn test_class_with_meta_field() { --- @class Tracked local _CLASS_Tracked_ = { - __metatable = { - --- Meta count - --- @type integer - __count = nil, - } +\t__metatable = { +\t\t--- Meta count +\t\t--- @type integer +\t\t__count = nil, +\t} }" ); } @@ -591,7 +607,8 @@ fn test_class_with_meta_method() { Type::class( TypedClassBuilder::default() .field("x", Type::number(), ()) - .meta_method::<(), String>("__tostring", ()), + .meta_method::<(), String>("__tostring", ()) + .build(), ), ), )); @@ -602,11 +619,11 @@ fn test_class_with_meta_method() { --- @class Obj --- @field x number local _CLASS_Obj_ = { - __metatable = { - --- @param self Obj - --- @return string - __tostring = function(self) end, - } +\t__metatable = { +\t\t--- @param self Obj +\t\t--- @return string +\t\t__tostring = function(self) end, +\t} }" ); } @@ -618,7 +635,8 @@ fn test_class_with_meta_function() { "Indexed", Type::class( TypedClassBuilder::default() - .meta_function::<(String,), String>("__index", ()), + .meta_function::<(String,), String>("__index", ()) + .build(), ), ), )); @@ -628,11 +646,11 @@ fn test_class_with_meta_function() { --- @class Indexed local _CLASS_Indexed_ = { - __metatable = { - --- @param param1 string - --- @return string - __index = function(param1) end, - } +\t__metatable = { +\t\t--- @param param1 string +\t\t--- @return string +\t\t__index = function(param1) end, +\t} }" ); } @@ -646,7 +664,8 @@ fn test_type_sig_array() { "Names", Type::class( TypedClassBuilder::default() - .field("items", Type::array(Type::string()), ()), + .field("items", Type::array(Type::string()), ()) + .build(), ), ), )); @@ -666,7 +685,12 @@ fn test_type_sig_tuple() { "Pair", Type::class( TypedClassBuilder::default() - .field("coords", Type::tuple([Type::integer(), Type::integer()]), ()), + .field( + "coords", + Type::tuple([Type::integer(), Type::integer()]), + (), + ) + .build(), ), ), )); @@ -686,7 +710,8 @@ fn test_type_sig_map() { "Registry", Type::class( TypedClassBuilder::default() - .field("data", Type::map(Type::string(), Type::number()), ()), + .field("data", Type::map(Type::string(), Type::number()), ()) + .build(), ), ), )); @@ -705,14 +730,16 @@ fn test_type_sig_table() { Definition::start().register_as( "Config", Type::class( - TypedClassBuilder::default().field( - "opts", - Type::table([ - (Index::from("host"), Type::string()), - (Index::from("port"), Type::integer()), - ]), - (), - ), + TypedClassBuilder::default() + .field( + "opts", + Type::table([ + (Index::from("host"), Type::string()), + (Index::from("port"), Type::integer()), + ]), + (), + ) + .build(), ), ), )); @@ -732,7 +759,8 @@ fn test_type_sig_union() { "Container", Type::class( TypedClassBuilder::default() - .field("value", Type::string() | Type::nil(), ()), + .field("value", Type::string() | Type::nil(), ()) + .build(), ), ), )); @@ -753,21 +781,23 @@ fn test_type_sig_function_inline() { Definition::start().register_as( "Handler", Type::class( - TypedClassBuilder::default().field( - "callback", - Type::Function { - params: vec![Param { - name: Some("x".into()), - ty: Type::number(), - doc: None, - }], - returns: vec![Return { - ty: Type::boolean(), - doc: None, - }], - }, - (), - ), + TypedClassBuilder::default() + .field( + "callback", + Type::Function { + params: vec![Param { + name: Some("x".into()), + ty: Type::number(), + doc: None, + }], + returns: vec![Return { + ty: Type::boolean(), + doc: None, + }], + }, + (), + ) + .build(), ), ), )); @@ -790,7 +820,11 @@ fn test_type_sig_enum_cross_reference() { .register_as("Color", color_enum.clone()) .register_as( "Widget", - Type::class(TypedClassBuilder::default().field("color", color_enum, ())), + Type::class( + TypedClassBuilder::default() + .field("color", color_enum, ()) + .build(), + ), ), )); assert_eq!( @@ -812,14 +846,19 @@ fn test_type_sig_class_cross_reference() { let vec2 = Type::class( TypedClassBuilder::default() .field("x", Type::number(), ()) - .field("y", Type::number(), ()), + .field("y", Type::number(), ()) + .build(), ); let out = generate(single( Definition::start() .register_as("Vec2", vec2.clone()) .register_as( "Sprite", - Type::class(TypedClassBuilder::default().field("position", vec2, ())), + Type::class( + TypedClassBuilder::default() + .field("position", vec2, ()) + .build(), + ), ), )); assert_eq!( @@ -889,8 +928,12 @@ fn test_luals_function_wrong_arg_type() { let log_dir = tempfile::TempDir::new().unwrap(); let meta_dir = tempfile::TempDir::new().unwrap(); std::fs::write(dir.path().join("defs.d.lua"), &out).unwrap(); - std::fs::write(dir.path().join("test.lua"), "greet(42) -").unwrap(); + std::fs::write( + dir.path().join("test.lua"), + "greet(42) +", + ) + .unwrap(); std::fs::write( dir.path().join(".luarc.json"), r#"{"workspace.library": ["./"]}"#, @@ -937,7 +980,8 @@ fn test_luals_class_field_access() { Type::class( TypedClassBuilder::default() .field("name", Type::string(), ()) - .field("score", Type::integer(), ()), + .field("score", Type::integer(), ()) + .build(), ), ), "player", @@ -961,7 +1005,8 @@ fn test_luals_class_method_call() { "Player", Type::class( TypedClassBuilder::default() - .method::<(), String>("getName", ()), + .method::<(), String>("getName", ()) + .build(), ), ), "player", @@ -987,8 +1032,11 @@ fn test_luals_enum_valid_assignment() { Type::named("Direction"), None, ))); - validate_with_lua_ls(&out, "local _d = dir -"); + validate_with_lua_ls( + &out, + "local _d = dir +", + ); } #[test] @@ -1050,7 +1098,11 @@ fn test_luals_enum_referenced_in_class_field() { .register_as("Color", color_enum.clone()) .register_as( "Widget", - Type::class(TypedClassBuilder::default().field("color", color_enum, ())), + Type::class( + TypedClassBuilder::default() + .field("color", color_enum, ()) + .build(), + ), ), "widget", Type::named("Widget"), diff --git a/src/typed/mod.rs b/src/typed/mod.rs index 45a9c56..63325c7 100644 --- a/src/typed/mod.rs +++ b/src/typed/mod.rs @@ -4,7 +4,7 @@ pub mod generator; mod class; pub use class::{ - TypedClassBuilder, TypedDataDocumentation, TypedDataFields, TypedDataMethods, TypedUserData, + TypedClassBuilder, TypedClass, TypedDataDocumentation, TypedDataFields, TypedDataMethods, TypedUserData, WrappedBuilder, }; @@ -21,9 +21,9 @@ use mlua::{IntoLua, MetaMethod, Value, Variadic}; /// Represents a lua table key /// /// Table keys can be either a string or an integer -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, strum::EnumIs)] pub enum Index { - Int(usize), + Int(isize), Str(Cow<'static, str>), } @@ -75,8 +75,8 @@ impl From for Index { } } -impl From for Index { - fn from(value: usize) -> Self { +impl From for Index { + fn from(value: isize) -> Self { Self::Int(value) } } @@ -187,7 +187,7 @@ pub enum Type { /// --- @field age integer /// --- @field height number /// ``` - Class(Box), + Class(Box), } /// Allows to union types @@ -327,7 +327,7 @@ impl Type { } /// create a type that is a class. i.e. `--- @class {name}` - pub fn class(class: TypedClassBuilder) -> Self { + pub fn class(class: TypedClass) -> Self { Self::Class(Box::new(class)) } @@ -869,6 +869,25 @@ impl Field { } } +/// Type information for a lua `class` field +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct StaticField { + pub inner: Field, + pub default: Cow<'static, str>, +} + +impl StaticField { + pub fn new(ty: Type, doc: impl IntoDocComment, default: impl Into>) -> Self { + Self { + inner: Field { + ty, + doc: doc.into_doc_comment(), + }, + default: default.into() + } + } +} + /// Type information for a lua `class` function #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Func { diff --git a/tests/typed_user_data.rs b/tests/typed_user_data.rs new file mode 100644 index 0000000..92156bb --- /dev/null +++ b/tests/typed_user_data.rs @@ -0,0 +1,493 @@ +#![cfg(all(feature = "mlua", feature = "macros"))] + +use mlua::{AnyUserData, FromLua}; +use mlua_extras::{ + TypedUserData, + mlua::{self, Lua, Value}, + typed_user_data_impl, +}; + +// Test 1: Feild attribute parsing + +#[allow(dead_code)] +#[derive(Clone, TypedUserData)] +struct TestNamedFields { + normal: String, + #[field(skip)] + skipped: bool, + #[field(readonly)] + readonly: i32, + #[field(writeonly)] + writeonly: f64, + #[field(rename = "colour")] + color: String, + #[field(rename = -1)] + value: Option, +} + +#[test] +fn test_named_field_registration() { + let lua = Lua::new(); + + lua.globals() + .set( + "obj", + TestNamedFields { + normal: "test".into(), + skipped: true, + readonly: 4, + writeonly: 3.14, + color: "red".into(), + value: Some("Hello, world!".into()), + }, + ) + .unwrap(); + + // Read + Write field + let val: String = lua.load("return obj.normal").eval().unwrap(); + assert_eq!(val, "test"); + lua.load("obj.normal = 'testing'").exec().unwrap(); + let val: String = lua.load("return obj.normal").eval().unwrap(); + assert_eq!(val, "testing"); + + // Skip field: not accessible (throws error) + let result = lua.load("return obj.skipped").eval::(); + println!("{result:?}"); + assert!(result.is_err()); + + // Readonly field: Fails on write + let val: i32 = lua.load("return obj.readonly").eval().unwrap(); + assert_eq!(val, 4); + let result = lua.load("obj.readonly = 100").exec(); + assert!(result.is_err()); + + // Writeonly field: Fails on read + let result = lua.load("return obj.writeonly").eval::(); + assert!(result.is_err()); + let result = lua.load("obj.writeonly = 6.28").exec(); + assert!(result.is_ok()); + + // Renamed field: accessible only via the rename value + let val: String = lua.load("return obj.colour").eval().unwrap(); + assert_eq!(val, "red"); + lua.load("obj.colour = 'blue'").exec().unwrap(); + let val: String = lua.load("return obj.colour").eval().unwrap(); + assert_eq!(val, "blue"); + + let result = lua.load("return obj.color").eval::(); + assert!(result.is_err()); + + // Rename field: Named fields renamed to an index are only indexable + let val: Option = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val.as_deref(), Some("Hello, world!")); + lua.load("obj[-1] = nil").exec().unwrap(); + let val: Option = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, None); + let result = lua.load("return obj.value").eval::(); + assert!(result.is_err()); +} + +#[allow(dead_code)] +#[derive(Clone, TypedUserData)] +struct TestIndexedFields( + String, + #[field(skip)] bool, + #[field(readonly)] i32, + #[field(writeonly)] f64, + #[field(rename = -1)] String, + #[field(rename = "value")] Option, +); + +#[test] +fn test_indexed_field_registration() { + let lua = Lua::new(); + + lua.globals() + .set( + "obj", + TestIndexedFields( + "test".into(), + true, + 4, + 3.14, + "red".into(), + Some("Hello, world!".into()), + ), + ) + .unwrap(); + + // Read + Write field + let val: String = lua.load("return obj[1]").eval().unwrap(); + assert_eq!(val, "test"); + lua.load("obj[1] = 'testing'").exec().unwrap(); + let val: String = lua.load("return obj[1]").eval().unwrap(); + assert_eq!(val, "testing"); + + // Skip field: not accessible (throws error) + let result = lua.load("return obj[2]").eval::(); + assert!(result.is_err()); + + // Readonly field: Fails on write + let val: i32 = lua.load("return obj[3]").eval().unwrap(); + assert_eq!(val, 4); + let result = lua.load("obj[3] = 100").exec(); + assert!(result.is_err()); + + // Writeonly field: Fails on read + let result = lua.load("return obj[4]").eval::(); + assert!(result.is_err()); + let result = lua.load("obj[4] = 6.28").exec(); + assert!(result.is_ok()); + + // Renamed field: accessible only via the rename value + let val: String = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, "red"); + lua.load("obj[-1] = 'blue'").exec().unwrap(); + let val: String = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, "blue"); + + let result = lua.load("return obj[5]").eval::(); + assert!(result.is_err()); + + // Rename field: Named fields renamed to an index are only indexable + let val: Option = lua.load("return obj.value").eval().unwrap(); + assert_eq!(val.as_deref(), Some("Hello, world!")); + lua.load("obj.value = nil").exec().unwrap(); + let val: Option = lua.load("return obj.value").eval().unwrap(); + assert_eq!(val, None); + let result = lua.load("return obj[6]").eval::(); + assert!(result.is_err()); +} + +// Test 2: Methods with rename + +#[derive(Clone, TypedUserData)] +struct Calculator { + value: f64, +} + +#[typed_user_data_impl] +impl Calculator { + #[method] + fn add(&self, x: f64) -> f64 { + self.value + x + } + + #[method(rename = "divide")] + fn checked_divide(&self, x: f64) -> mlua::Result { + if x == 0.0 { + Err(mlua::Error::runtime("division by zero")) + } else { + Ok(self.value / x) + } + } + + #[method] + fn get_value_and_double(&self) -> (f64, f64) { + (self.value, self.value * 2.0) + } +} + +#[test] +fn test_method_registration() { + let lua = Lua::new(); + + lua.globals() + .set("calc", Calculator { value: 10.0 }) + .unwrap(); + + let val: f64 = lua.load("return calc.value").eval().unwrap(); + assert_eq!(val, 10.0); + + // Infallible + let result: f64 = lua.load("return calc:add(5)").eval().unwrap(); + assert_eq!(result, 15.0); + + // Rename (fallible) + let result: f64 = lua.load("return calc:divide(2)").eval().unwrap(); + assert_eq!(result, 5.0); + + // Fallible + let result = lua.load("return calc:divide(0)").exec(); + assert!(result.is_err()); + + // Multi-return method + let (a, b): (f64, f64) = lua + .load("return calc:get_value_and_double()") + .eval() + .unwrap(); + assert_eq!(a, 10.0); + assert_eq!(b, 20.0); +} + +// Test 3: Metamethods + +#[derive(Clone, TypedUserData)] +struct Stringable { + value: String, +} + +#[typed_user_data_impl] +impl Stringable { + #[metamethod(ToString)] + fn to_string_repr(&self) -> String { + format!("Stringable({})", self.value) + } + + #[metamethod(Len)] + fn len(&self) -> usize { + self.value.len() + } + + #[metamethod("__half")] + fn first_half(&self) -> String { + let c = self.len(); + self.value[0..c / 2].to_string() + } +} + +#[test] +fn test_metamethods() { + let lua = Lua::new(); + lua.globals() + .set( + "obj", + Stringable { + value: "hello, world!".into(), + }, + ) + .unwrap(); + lua.globals() + .set( + "half", + lua.create_function(|_lua, this: AnyUserData| { + let metatable = this.metatable()?; + if let Ok(half) = metatable.get::("__half") { + return half.call::(this); + } + Err(mlua::Error::runtime( + "type does not implememnt __half metamethod", + )) + }) + .unwrap(), + ) + .unwrap(); + + let result: String = lua.load("return tostring(obj)").eval().unwrap(); + assert_eq!(result, "Stringable(hello, world!)"); + + let result: i64 = lua.load("return #obj").eval().unwrap(); + assert_eq!(result, 13); + + let result: String = lua.load("return half(obj)").eval().unwrap(); + assert_eq!(result, "hello,"); +} + +// Test 4: Mutable Methods + +#[derive(Clone, TypedUserData)] +struct MutCalc { + value: f64, +} + +#[typed_user_data_impl] +impl MutCalc { + #[method] + fn set_value(&mut self, x: f64) { + self.value = x; + } +} + +#[test] +fn test_mut_method() { + let lua = Lua::new(); + lua.globals().set("calc", MutCalc { value: 0.0 }).unwrap(); + + lua.load("calc:set_value(42)").exec().unwrap(); + let result: f64 = lua.load("return calc.value").eval().unwrap(); + assert_eq!(result, 42.0); +} + +// Test 5: Optional lua parameter + +#[derive(Clone, TypedUserData)] +struct LuaAccess; + +#[typed_user_data_impl] +impl LuaAccess { + #[method] + fn create_table(&self, lua: &Lua) -> mlua::Result { + lua.create_table() + } + + #[method] + fn no_lua(&self) -> String { + "test".into() + } +} + +#[test] +fn test_optional_lua_param() { + let lua = Lua::new(); + lua.globals().set("obj", LuaAccess).unwrap(); + let result: mlua::Table = lua.load("return obj:create_table()").eval().unwrap(); + assert!(result.is_empty()); + let result: String = lua.load("return obj:no_lua()").eval().unwrap(); + assert_eq!(result, "test"); +} + +// Test 6: Static functions (no self) + +#[derive(Clone, TypedUserData)] +struct MathUtils; + +#[typed_user_data_impl] +impl MathUtils { + #[method] + fn add(a: f64, b: f64) -> f64 { + a + b + } + + #[method(rename = "create")] + fn new_instance(lua: &Lua) -> mlua::Result { + lua.create_table() + } +} + +#[test] +fn test_static_functions() { + let lua = Lua::new(); + lua.globals().set("math", MathUtils).unwrap(); + + let result: f64 = lua.load("return math.add(3, 4)").eval().unwrap(); + assert_eq!(result, 7.0); + + let result: mlua::Table = lua.load("return math.create()").eval().unwrap(); + assert!(result.is_empty()); +} + +// Test 7: Static meta functions (no self) + +#[derive(Debug, Clone, TypedUserData, PartialEq)] +struct Vec2(f64, f64); +impl FromLua for Vec2 { + fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { + let tn = value.type_name(); + match value { + Value::UserData(usr_data) => { + if usr_data.is::() { + return usr_data.take::(); + } + } + Value::Table(tbl) => { + return Ok(Vec2(tbl.get(1)?, tbl.get(2)?)); + } + Value::Number(n) => return Ok(Vec2(n, n)), + _ => (), + } + + Err(mlua::Error::FromLuaConversionError { + from: tn, + to: "Vec2".to_string(), + message: Some("failed to convert to userdata Vec2".into()), + }) + } +} + +#[typed_user_data_impl] +impl Vec2 { + #[metamethod(Add)] + fn add(a: Self, b: Self) -> Self { + Vec2(a.0 + b.0, a.1 + b.1) + } + + #[metamethod("__dot")] + fn dot_product(a: Self, b: Self) -> f64 { + (a.0 * b.0) + (a.1 * b.1) + } +} + +#[test] +fn test_static_meta_functions() { + let lua = Lua::new(); + lua.globals() + .set( + "vec2", + lua.create_function(|_lua, (x, y): (f64, f64)| Ok(Vec2(x, y))) + .unwrap(), + ) + .unwrap(); + lua.globals() + .set( + "dot", + lua.create_function(|_lua, (a, b): (AnyUserData, AnyUserData)| { + if a.type_id() != b.type_id() { + return Err(mlua::Error::runtime("both parameters but be the same type")); + } + + let am = a.metatable()?; + am.get::("__dot")?.call::((a, b)) + }) + .unwrap(), + ) + .unwrap(); + + let result: Vec2 = lua.load("return vec2(1, 2) + vec2(3, 4)").eval().unwrap(); + assert_eq!(result, Vec2(4.0, 6.0)); + + let result: f64 = lua.load("return dot(vec2(2, 4), vec2(4, 2))").eval().unwrap(); + assert_eq!(result, 16.0); +} + +// Test 8: Async Methods + +#[cfg(feature = "async")] +mod async_tests { + use super::*; + + #[derive(Clone, TypedUserData)] + struct AsyncWorker { + prefix: String, + } + + #[typed_user_data_impl] + impl AsyncWorker { + #[method] + async fn process(&self, input: String) -> mlua::Result { + Ok(format!("{}: {input}", self.prefix)) + } + + #[method] + async fn with_lua(&self, lua: Lua, key: String) -> mlua::Result { + lua.globals().get(key) + } + } + + #[tokio::test] + async fn test_async_methods() { + let lua = Lua::new(); + lua.globals() + .set( + "worker", + AsyncWorker { + prefix: "test".into(), + }, + ) + .unwrap(); + + let result: String = lua + .load("return worker:process('hello, world')") + .eval_async() + .await + .unwrap(); + assert_eq!(result, "test: hello, world"); + + lua.globals().set("test_val", 42).unwrap(); + let result: i64 = lua + .load("return worker:with_lua('test_val')") + .eval_async() + .await + .unwrap(); + assert_eq!(result, 42) + } +} diff --git a/tests/user_data.rs b/tests/user_data.rs index 97c09ed..dbd51f2 100644 --- a/tests/user_data.rs +++ b/tests/user_data.rs @@ -7,7 +7,7 @@ use mlua_extras::{ user_data_impl, }; -// Test 1: Feidl attribute parsing +// Test 1: Feild attribute parsing #[allow(dead_code)] #[derive(Clone, UserData)]