1use proc_macro::TokenStream;
45use quote::quote;
46use syn::{Data, DeriveInput, Fields, Lit, parse_macro_input};
47
48#[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
61struct 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
93struct 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 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
142struct FieldInfo {
145 ident: syn::Ident,
147 script_name: String,
149 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 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 let type_name_impl = quote! {
199 fn type_name(&self) -> &str {
200 #type_name_str
201 }
202 };
203
204 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 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 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 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 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 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 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}