nanom_derive/
lib.rs

1use convert_case::{Case, Casing};
2use proc_macro::TokenStream;
3use syn::{ext::IdentExt, Data, DataEnum, DataStruct, Fields};
4use synstructure::{quote, BindStyle, Structure};
5
6fn derive_js_object(mut input: Structure) -> TokenStream {
7    for ty in input.referenced_ty_params() {
8        input.add_where_predicate(syn::parse_quote! { #ty: ::nanom::IntoJs + ::nanom::FromJs });
9    }
10
11    let kind = input.each_variant(|variant| {
12        let name = variant.ast().ident.unraw().to_string();
13        quote! { env.create_string(#name)? }
14    });
15
16    input.bind_with(|_| BindStyle::Move);
17
18    let set_props = input.each(|field| {
19        let name = field.ast().ident.as_ref().expect("Tuple structs cannot be used to derive JsObject").unraw().to_string();
20        let name_camel = name.to_case(Case::Camel);
21        let binding = &field.binding;
22
23        quote! {
24            (|| -> ::std::result::Result<(), ::nanom::ConversionError> {
25            env.set_property(object, env.create_string(#name_camel)?, ::nanom::IntoJs::into_js(#binding, env)?)?;
26            ::std::result::Result::Ok(())
27            })().map_err(|err| ::nanom::ConversionError::InObjectField { error: Box::new(err), field_name: #name })?;
28        }
29    });
30
31    let get_object = quote! {
32        {
33            let mut object = env.create_object()?;
34
35            match self {
36                #set_props
37            }
38
39            object
40        }
41    };
42
43    let to_js = match input.ast().data {
44        Data::Union(_) => panic!("Unions cannot be used to derive JsObject"),
45        Data::Struct(_) => get_object,
46        Data::Enum(_) => quote! {
47            {
48                let mut object = env.create_object()?;
49
50                (|| -> ::std::result::Result<(), ::nanom::napi::Status> {
51                    env.set_property(object, env.create_string("kind")?, match self { #kind })?;
52                    ::std::result::Result::Ok(())
53                })().map_err(::nanom::ConversionError::InKind)?;
54
55                (|| -> ::std::result::Result<(), ::nanom::ConversionError> {
56                    env.set_property(object, env.create_string("fields")?, #get_object)?;
57                    ::std::result::Result::Ok(())
58                })().map_err(|err|::nanom::ConversionError::InEnumValue(Box::new(err)))?;
59
60                object
61            }
62        },
63    };
64
65    let from_js = match &input.ast().data {
66        Data::Union(_) => panic!("Unions cannot be used to derive JsObject"),
67        Data::Struct(DataStruct { fields, .. }) => {
68            let parse_fields = parse_fields(&fields);
69
70            quote! {
71                {
72                    Self { #parse_fields }
73                }
74            }
75        }
76        Data::Enum(DataEnum { variants, .. }) => {
77            let parse_variants = variants.into_iter().map(|variant| {
78                let name = &variant.ident;
79                let name_str = name.unraw().to_string();
80
81                let parse_fields = parse_fields(&variant.fields);
82
83                quote! {
84                    #name_str => (|| -> ::std::result::Result<_, ::nanom::ConversionError> {
85                        ::std::result::Result::Ok(Self::#name { #parse_fields })
86                    })().map_err(|err|::nanom::ConversionError::InEnumValue(Box::new(err)))?,
87                }
88            });
89
90            quote! {
91                {
92                    let kind = (|| -> ::std::result::Result<_, ::nanom::napi::Status> {
93                        ::std::result::Result::Ok(env.get_value_string(env.get_property(value, env.create_string("kind")?)?)?)
94                    })().map_err(::nanom::ConversionError::InKind)?;
95
96                    let value = (|| -> ::std::result::Result<_, ::nanom::ConversionError> {
97                        ::std::result::Result::Ok(env.get_property(value, env.create_string("fields")?)?)
98                    })().map_err(|err|::nanom::ConversionError::InEnumValue(Box::new(err)))?;
99
100                    match kind.as_str() {
101                        #(#parse_variants)*
102                        _ => return ::std::result::Result::Err(::nanom::ConversionError::InvalidKind(kind.to_string())),
103                    }
104                }
105            }
106        }
107    };
108
109    let type_name = input.ast().ident.unraw().to_string();
110
111    let ts_type = match &input.ast().data {
112        Data::Union(_) => panic!("Unions cannot be used to derive JsObject"),
113        Data::Struct(DataStruct { fields, .. }) => {
114            let fields = fields_ts_type(&fields);
115            quote! { ::nanom::typing::Type::NamedObject { name: #type_name.to_string(), fields: #fields, id: ::std::any::TypeId::of::<Self>() } }
116        }
117        Data::Enum(DataEnum { variants, .. }) => {
118            let add_variants = variants.into_iter().map(|variant| {
119                let name_str = variant.ident.unraw().to_string();
120
121                let fields_type = fields_ts_type(&variant.fields);
122
123                quote! {
124                    {
125                        enum_fields.insert(#name_str.to_string(), #fields_type); 
126                    }
127                }
128            });
129
130            quote! {
131                {
132                    let mut enum_fields = ::std::collections::HashMap::new();
133                    #(#add_variants)*
134                    ::nanom::typing::Type::Enum { kinds: enum_fields, name: #type_name.to_string(), id: ::std::any::TypeId::of::<Self>() }
135                }
136            }
137        }
138    };
139
140    input
141        .unbound_impl(quote!(::nanom::JsObject), quote! {
142            fn into_js(self, env: ::nanom::napi::Env) -> ::std::result::Result<::nanom::napi::Value, ::nanom::ConversionError> {
143                ::std::result::Result::Ok(unsafe #to_js)
144            }
145            
146            fn from_js(env: ::nanom::napi::Env, value: ::nanom::napi::Value) -> ::std::result::Result<Self, ::nanom::ConversionError> {
147                ::std::result::Result::Ok(unsafe #from_js)
148            }
149
150            fn ts_type() -> ::nanom::typing::Type {
151                #ts_type
152            }
153        })
154        .into()
155}
156
157fn parse_fields(fields: &Fields) -> proc_macro2::TokenStream {
158    let Fields::Named(fields) = fields else {
159        panic!("Only named structs can be used to derive JsObject");
160    };
161
162    let fields = fields.named.iter().map(|field| {
163        let name = field.ident.as_ref().unwrap();
164        let name_str = name.unraw().to_string();
165        let name_str_camel = name_str.to_case(Case::Camel);
166
167        quote! {
168            #name: (|| -> ::std::result::Result<_, ::nanom::ConversionError> {
169                ::nanom::FromJs::from_js(env, env.get_property(value, env.create_string(#name_str_camel)?)?)
170            })().map_err(|err| ::nanom::ConversionError::InObjectField { error: Box::new(err), field_name: #name_str })?,
171        }
172    });
173
174    quote! {
175        #(#fields)*
176    }
177}
178
179fn fields_ts_type(fields: &Fields) -> proc_macro2::TokenStream {
180    let Fields::Named(fields) = fields else {
181        panic!("Only named structs can be used to derive JsObject");
182    };
183
184    let fields = fields.named.iter().map(|field| {
185        let name = field.ident.as_ref().unwrap().unraw().to_string().to_case(Case::Camel);
186        let ty = &field.ty;
187
188        quote! {
189            fields.insert(#name.to_string(), <#ty as ::nanom::TsType>::ts_type_ref());
190        }
191    });
192
193    quote! {
194        {
195            let mut fields = ::std::collections::HashMap::new();
196            #(#fields)*
197            fields
198        }
199    }
200}
201
202synstructure::decl_derive!([JsObject] => derive_js_object);