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);