Skip to main content

urd_derive/
lib.rs

1//! # urd-derive
2//!
3//! Procedural derive macro for the [`ExternObject`] trait from the `urd`
4//! scripting language runtime.
5//!
6//! ## Usage
7//!
8//! ```rust,ignore
9//! use urd::prelude::*;
10//!
11//! #[derive(ExternObject)]
12//! #[extern_object(type_name = "Player")]
13//! struct Player {
14//!     hp: i32,
15//!     name: String,
16//!
17//!     #[extern_object(readonly)]
18//!     id: u64,
19//!
20//!     #[extern_object(skip)]
21//!     internal: SomeInternalType,
22//!
23//!     #[extern_object(rename = "pos_x")]
24//!     position_x: f64,
25//! }
26//! ```
27//!
28//! ## Attributes
29//!
30//! ### Container-level (`#[extern_object(...)]` on the struct)
31//!
32//! | Attribute | Description |
33//! |-----------|-------------|
34//! | `type_name = "..."` | Override the type name returned by `ExternObject::type_name()`. Defaults to the struct name. |
35//!
36//! ### Field-level (`#[extern_object(...)]` on a field)
37//!
38//! | Attribute | Description |
39//! |-----------|-------------|
40//! | `skip` | Omit this field entirely — not readable, not writable, not listed. |
41//! | `readonly` | Field is readable but not writable from scripts. |
42//! | `rename = "..."` | Expose the field under a different name in scripts. |
43
44use proc_macro::TokenStream;
45use quote::quote;
46use syn::{Data, DeriveInput, Fields, Lit, parse_macro_input};
47
48/// Derive the `ExternObject` trait for a struct.
49///
50/// See the [module-level documentation](crate) for usage and supported
51/// attributes.
52#[proc_macro_derive(ExternObject, attributes(extern_object))]
53pub fn derive_extern_object(input: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(input as DeriveInput);
55    match impl_extern_object(&input) {
56        Ok(tokens) => tokens.into(),
57        Err(err) => err.to_compile_error().into(),
58    }
59}
60
61// ── Container-level attribute parsing ─────────────────────────────────────────
62
63struct ContainerAttrs {
64    type_name: Option<String>,
65}
66
67fn parse_container_attrs(attrs: &[syn::Attribute]) -> syn::Result<ContainerAttrs> {
68    let mut type_name = None;
69
70    for attr in attrs {
71        if !attr.path().is_ident("extern_object") {
72            continue;
73        }
74        attr.parse_nested_meta(|meta| {
75            if meta.path.is_ident("type_name") {
76                let value = meta.value()?;
77                let lit: Lit = value.parse()?;
78                if let Lit::Str(s) = lit {
79                    type_name = Some(s.value());
80                    Ok(())
81                } else {
82                    Err(meta.error("expected string literal for `type_name`"))
83                }
84            } else {
85                Err(meta.error("unknown extern_object container attribute"))
86            }
87        })?;
88    }
89
90    Ok(ContainerAttrs { type_name })
91}
92
93// ── Field-level attribute parsing ─────────────────────────────────────────────
94
95struct FieldAttrs {
96    skip: bool,
97    readonly: bool,
98    rename: Option<String>,
99}
100
101fn parse_field_attrs(attrs: &[syn::Attribute]) -> syn::Result<FieldAttrs> {
102    let mut skip = false;
103    let mut readonly = false;
104    let mut rename = None;
105
106    for attr in attrs {
107        if !attr.path().is_ident("extern_object") {
108            continue;
109        }
110
111        // Handle bare `#[extern_object(skip)]` / `#[extern_object(readonly)]`
112        // as well as `#[extern_object(rename = "...")]`.
113        attr.parse_nested_meta(|meta| {
114            if meta.path.is_ident("skip") {
115                skip = true;
116                Ok(())
117            } else if meta.path.is_ident("readonly") {
118                readonly = true;
119                Ok(())
120            } else if meta.path.is_ident("rename") {
121                let value = meta.value()?;
122                let lit: Lit = value.parse()?;
123                if let Lit::Str(s) = lit {
124                    rename = Some(s.value());
125                    Ok(())
126                } else {
127                    Err(meta.error("expected string literal for `rename`"))
128                }
129            } else {
130                Err(meta.error("unknown extern_object field attribute"))
131            }
132        })?;
133    }
134
135    Ok(FieldAttrs {
136        skip,
137        readonly,
138        rename,
139    })
140}
141
142// ── Code generation ───────────────────────────────────────────────────────────
143
144struct FieldInfo {
145    /// The Rust field identifier (for `self.field_name`).
146    ident: syn::Ident,
147    /// The script-visible name (after rename).
148    script_name: String,
149    /// Whether the field is read-only from scripts.
150    readonly: bool,
151}
152
153fn impl_extern_object(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
154    let struct_name = &input.ident;
155    let container = parse_container_attrs(&input.attrs)?;
156    let type_name_str = container
157        .type_name
158        .unwrap_or_else(|| struct_name.to_string());
159
160    let fields = match &input.data {
161        Data::Struct(data) => match &data.fields {
162            Fields::Named(named) => &named.named,
163            _ => {
164                return Err(syn::Error::new_spanned(
165                    struct_name,
166                    "ExternObject can only be derived for structs with named fields",
167                ));
168            }
169        },
170        _ => {
171            return Err(syn::Error::new_spanned(
172                struct_name,
173                "ExternObject can only be derived for structs",
174            ));
175        }
176    };
177
178    // Collect field metadata, filtering out skipped fields.
179    let mut field_infos: Vec<FieldInfo> = Vec::new();
180    for field in fields {
181        let attrs = parse_field_attrs(&field.attrs)?;
182        if attrs.skip {
183            continue;
184        }
185        let Some(ident) = field.ident.clone() else {
186            return Err(syn::Error::new_spanned(field, "named field must have an ident"));
187        };
188        let script_name = attrs.rename.unwrap_or_else(|| ident.to_string());
189        field_infos.push(FieldInfo {
190            ident,
191            script_name,
192            readonly: attrs.readonly,
193        });
194    }
195
196    // ── type_name() ───────────────────────────────────────────────────────
197
198    let type_name_impl = quote! {
199        fn type_name(&self) -> &str {
200            #type_name_str
201        }
202    };
203
204    // ── display() ─────────────────────────────────────────────────────────
205
206    let display_fields: Vec<proc_macro2::TokenStream> = field_infos
207        .iter()
208        .map(|fi| {
209            let ident = &fi.ident;
210            let name = &fi.script_name;
211            quote! {
212                parts.push(format!(
213                    "{}: {}",
214                    #name,
215                    ::urd::runtime::extern_object::display_brief(
216                        &::urd::runtime::extern_object::IntoRuntimeValue::to_runtime_value(&self.#ident)
217                    )
218                ));
219            }
220        })
221        .collect();
222
223    // Check if we have any fields to display to avoid unused variable warning.
224    let display_impl = if display_fields.is_empty() {
225        quote! {
226            fn display(&self) -> String {
227                format!("<{}>", #type_name_str)
228            }
229        }
230    } else {
231        quote! {
232            fn display(&self) -> String {
233                let mut parts: Vec<String> = Vec::new();
234                #(#display_fields)*
235                format!("{} {{ {} }}", #type_name_str, parts.join(", "))
236            }
237        }
238    };
239
240    // ── get() ─────────────────────────────────────────────────────────────
241
242    let get_arms: Vec<proc_macro2::TokenStream> = field_infos
243        .iter()
244        .map(|fi| {
245            let ident = &fi.ident;
246            let name = &fi.script_name;
247            quote! {
248                #name => ::std::result::Result::Ok(
249                    ::urd::runtime::extern_object::IntoRuntimeValue::to_runtime_value(&self.#ident)
250                ),
251            }
252        })
253        .collect();
254
255    let get_impl = quote! {
256        fn get(&self, field: &str) -> ::std::result::Result<::urd::RuntimeValue, String> {
257            match field {
258                #(#get_arms)*
259                other => ::std::result::Result::Err(
260                    format!("no field '{}' on {}", other, #type_name_str)
261                ),
262            }
263        }
264    };
265
266    // ── set() ─────────────────────────────────────────────────────────────
267
268    let set_arms: Vec<proc_macro2::TokenStream> = field_infos
269        .iter()
270        .map(|fi| {
271            let ident = &fi.ident;
272            let name = &fi.script_name;
273            if fi.readonly {
274                quote! {
275                    #name => ::std::result::Result::Err(
276                        format!("field '{}' on {} is read-only", #name, #type_name_str)
277                    ),
278                }
279            } else {
280                quote! {
281                    #name => {
282                        self.#ident = ::urd::runtime::extern_object::FromRuntimeValue::from_runtime_value(&value)?;
283                        ::std::result::Result::Ok(())
284                    }
285                }
286            }
287        })
288        .collect();
289
290    let set_impl = quote! {
291        fn set(&mut self, field: &str, value: ::urd::RuntimeValue) -> ::std::result::Result<(), String> {
292            match field {
293                #(#set_arms)*
294                other => ::std::result::Result::Err(
295                    format!("no field '{}' on {}", other, #type_name_str)
296                ),
297            }
298        }
299    };
300
301    // ── fields() ──────────────────────────────────────────────────────────
302
303    let field_names: Vec<&String> = field_infos.iter().map(|fi| &fi.script_name).collect();
304
305    let fields_impl = quote! {
306        fn fields(&self) -> Vec<String> {
307            vec![#(#field_names.to_string()),*]
308        }
309    };
310
311    // ── Assemble ──────────────────────────────────────────────────────────
312
313    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
314
315    let expanded = quote! {
316        impl #impl_generics ::urd::ExternObject for #struct_name #ty_generics #where_clause {
317            #type_name_impl
318            #display_impl
319            #get_impl
320            #set_impl
321            #fields_impl
322        }
323    };
324
325    // ── Bonus: auto-derive Send + Sync assertion ──────────────────────────
326    // ExternObject requires Send + Sync.  Rather than emitting a blanket
327    // `unsafe impl`, we just static-assert that the struct already is.
328    // If the user's struct isn't Send + Sync, they'll get a clear error
329    // at the assertion site rather than a cryptic trait-bound failure.
330
331    let assert_send_sync = quote! {
332        const _: () = {
333            fn _assert_send_sync<T: Send + Sync>() {}
334            fn _assert() {
335                _assert_send_sync::<#struct_name>();
336            }
337        };
338    };
339
340    Ok(quote! {
341        #expanded
342        #assert_send_sync
343    })
344}