1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument, Lit, PathArguments, Type,
5 parse_macro_input,
6};
7
8#[proc_macro_derive(ExcelRow, attributes(excel))]
9pub fn derive_excel_row(input: TokenStream) -> TokenStream {
10 let input = parse_macro_input!(input as DeriveInput);
11
12 expand_excel_row(&input)
13 .unwrap_or_else(syn::Error::into_compile_error)
14 .into()
15}
16
17fn expand_excel_row(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
18 let ident = &input.ident;
19 let fields = match &input.data {
20 Data::Struct(data) => match &data.fields {
21 Fields::Named(fields) => &fields.named,
22 _ => {
23 return Err(syn::Error::new_spanned(
24 input,
25 "ExcelRow can only be derived for structs with named fields",
26 ));
27 }
28 },
29 _ => {
30 return Err(syn::Error::new_spanned(
31 input,
32 "ExcelRow can only be derived for structs",
33 ));
34 }
35 };
36
37 let mut column_defs = Vec::with_capacity(fields.len());
38 let mut to_row_values = Vec::with_capacity(fields.len());
39 let mut from_row_fields = Vec::with_capacity(fields.len());
40
41 for field in fields {
42 let field_ident = field
43 .ident
44 .as_ref()
45 .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
46 let field_name = field_ident.to_string();
47 let attrs = ExcelAttrs::parse(field)?;
48 let header = attrs
49 .header
50 .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(header = \"...\")]"))?;
51 let order = attrs
52 .order
53 .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(order = ...)]"))?;
54
55 let default_tokens = attrs
56 .default
57 .as_ref()
58 .map(|value| quote! { Some(#value) })
59 .unwrap_or_else(|| quote! { None });
60
61 column_defs.push(quote! {
62 ::excelx::ColumnDef {
63 field: #field_name,
64 header: #header,
65 order: #order,
66 default: #default_tokens,
67 }
68 });
69
70 let conversion = FieldConversion::for_type(&field.ty)?;
71 to_row_values.push(conversion.to_cell_value(field_ident)?);
72 from_row_fields.push(conversion.build_field_initializer(field_ident, &field_name)?);
73 }
74
75 Ok(quote! {
76 impl ::excelx::ExcelRow for #ident {
77 fn columns() -> ::std::vec::Vec<::excelx::ColumnDef> {
78 ::std::vec![#(#column_defs),*]
79 }
80
81 fn to_row(&self) -> ::std::vec::Vec<::excelx::CellValue> {
82 ::std::vec![#(#to_row_values),*]
83 }
84
85 fn from_row(row: &::excelx::RowView) -> ::std::result::Result<Self, ::excelx::ExcelError> {
86 ::std::result::Result::Ok(Self {
87 #(#from_row_fields),*
88 })
89 }
90 }
91 })
92}
93
94#[derive(Default)]
95struct ExcelAttrs {
96 header: Option<String>,
97 order: Option<usize>,
98 default: Option<String>,
99}
100
101impl ExcelAttrs {
102 fn parse(field: &syn::Field) -> syn::Result<Self> {
103 let mut attrs = Self::default();
104
105 for attr in &field.attrs {
106 if !attr.path().is_ident("excel") {
107 continue;
108 }
109
110 attr.parse_nested_meta(|meta| {
111 if meta.path.is_ident("header") {
112 let value = meta.value()?;
113 attrs.header = Some(value.parse::<syn::LitStr>()?.value());
114 Ok(())
115 } else if meta.path.is_ident("order") {
116 let value = meta.value()?;
117 attrs.order = Some(parse_usize_lit(value.parse::<Expr>()?)?);
118 Ok(())
119 } else if meta.path.is_ident("default") {
120 let value = meta.value()?;
121 attrs.default = Some(value.parse::<syn::LitStr>()?.value());
122 Ok(())
123 } else {
124 Err(meta.error("unsupported excel attribute"))
125 }
126 })?;
127 }
128
129 Ok(attrs)
130 }
131}
132
133fn parse_usize_lit(expr: Expr) -> syn::Result<usize> {
134 match expr {
135 Expr::Lit(ExprLit {
136 lit: Lit::Int(value),
137 ..
138 }) => value.base10_parse(),
139 other => Err(syn::Error::new_spanned(
140 other,
141 "order must be an unsigned integer literal",
142 )),
143 }
144}
145
146enum FieldConversion<'a> {
147 String,
148 Integer(&'a Type),
149 Float(&'a Type),
150 Bool,
151 Option(Box<FieldConversion<'a>>),
152}
153
154impl<'a> FieldConversion<'a> {
155 fn for_type(ty: &'a Type) -> syn::Result<Self> {
156 if let Some(inner) = option_inner_type(ty) {
157 if option_inner_type(inner).is_some() {
158 return Err(syn::Error::new_spanned(
159 ty,
160 "nested Option fields are not supported",
161 ));
162 }
163
164 return Ok(Self::Option(Box::new(Self::for_type(inner)?)));
165 }
166
167 if type_is(ty, "String") {
168 return Ok(Self::String);
169 }
170
171 if type_is(ty, "bool") {
172 return Ok(Self::Bool);
173 }
174
175 if is_supported_integer(ty) {
176 return Ok(Self::Integer(ty));
177 }
178
179 if is_supported_float(ty) {
180 return Ok(Self::Float(ty));
181 }
182
183 Err(syn::Error::new_spanned(
184 ty,
185 "unsupported ExcelRow field type",
186 ))
187 }
188
189 fn to_cell_value(&self, field_ident: &syn::Ident) -> syn::Result<proc_macro2::TokenStream> {
190 match self {
191 Self::String => Ok(quote! { ::excelx::CellValue::String(self.#field_ident.clone()) }),
192 Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int(self.#field_ident.into()) }),
193 Self::Float(ty) if type_is(ty, "f32") => Ok(
194 quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(self.#field_ident)) },
195 ),
196 Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(self.#field_ident) }),
197 Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(self.#field_ident) }),
198 Self::Option(inner) => {
199 let value_ident = format_ident!("value");
200 let inner_tokens = inner.to_cell_value_for_value(&value_ident)?;
201 Ok(quote! {
202 match &self.#field_ident {
203 ::std::option::Option::Some(#value_ident) => #inner_tokens,
204 ::std::option::Option::None => ::excelx::CellValue::Empty,
205 }
206 })
207 }
208 }
209 }
210
211 fn to_cell_value_for_value(
212 &self,
213 value_ident: &syn::Ident,
214 ) -> syn::Result<proc_macro2::TokenStream> {
215 match self {
216 Self::String => Ok(quote! { ::excelx::CellValue::String(#value_ident.clone()) }),
217 Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int((*#value_ident).into()) }),
218 Self::Float(ty) if type_is(ty, "f32") => Ok(
219 quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(*#value_ident)) },
220 ),
221 Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(*#value_ident) }),
222 Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(*#value_ident) }),
223 Self::Option(_) => Err(syn::Error::new_spanned(
224 value_ident,
225 "nested Option fields are not supported",
226 )),
227 }
228 }
229
230 fn build_field_initializer(
231 &self,
232 field_ident: &syn::Ident,
233 field_name: &str,
234 ) -> syn::Result<proc_macro2::TokenStream> {
235 let value_expr = self.required_accessor_expr(field_name)?;
236 Ok(quote! { #field_ident: #value_expr })
237 }
238
239 fn required_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
240 match self {
241 Self::String => Ok(quote! { row.required_string(#field_name)? }),
242 Self::Integer(ty) if type_is(ty, "i64") => {
243 Ok(quote! { row.required_i64(#field_name)? })
244 }
245 Self::Integer(ty) => Ok(quote! {
246 row.required_i64(#field_name)?.try_into().map_err(|_| {
247 ::excelx::ExcelError::InvalidCellType {
248 row: row.row_number(),
249 column: row.header_for_field(#field_name),
250 expected: ::std::stringify!(#ty).to_owned(),
251 found: "integer out of range".to_owned(),
252 }
253 })?
254 }),
255 Self::Float(ty) if type_is(ty, "f32") => {
256 Ok(quote! { row.required_f64(#field_name)? as f32 })
257 }
258 Self::Float(_) => Ok(quote! { row.required_f64(#field_name)? }),
259 Self::Bool => Ok(quote! { row.required_bool(#field_name)? }),
260 Self::Option(inner) => inner.optional_accessor_expr(field_name),
261 }
262 }
263
264 fn optional_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
265 match self {
266 Self::String => Ok(quote! { row.optional_string(#field_name)? }),
267 Self::Integer(ty) if type_is(ty, "i64") => {
268 Ok(quote! { row.optional_i64(#field_name)? })
269 }
270 Self::Integer(ty) => Ok(quote! {
271 match row.optional_i64(#field_name)? {
272 ::std::option::Option::Some(value) => {
273 ::std::option::Option::Some(value.try_into().map_err(|_| {
274 ::excelx::ExcelError::InvalidCellType {
275 row: row.row_number(),
276 column: row.header_for_field(#field_name),
277 expected: ::std::stringify!(#ty).to_owned(),
278 found: "integer out of range".to_owned(),
279 }
280 })?)
281 }
282 ::std::option::Option::None => ::std::option::Option::None,
283 }
284 }),
285 Self::Float(ty) if type_is(ty, "f32") => {
286 Ok(quote! { row.optional_f64(#field_name)?.map(|value| value as f32) })
287 }
288 Self::Float(_) => Ok(quote! { row.optional_f64(#field_name)? }),
289 Self::Bool => Ok(quote! { row.optional_bool(#field_name)? }),
290 Self::Option(_) => Err(syn::Error::new_spanned(
291 field_name,
292 "nested Option fields are not supported",
293 )),
294 }
295 }
296}
297
298fn option_inner_type(ty: &Type) -> Option<&Type> {
299 let Type::Path(type_path) = ty else {
300 return None;
301 };
302
303 let segment = type_path.path.segments.last()?;
304 if segment.ident != "Option" {
305 return None;
306 }
307
308 let PathArguments::AngleBracketed(args) = &segment.arguments else {
309 return None;
310 };
311
312 match args.args.first()? {
313 GenericArgument::Type(inner) => Some(inner),
314 _ => None,
315 }
316}
317
318fn type_is(ty: &Type, expected: &str) -> bool {
319 let Type::Path(type_path) = ty else {
320 return false;
321 };
322
323 type_path
324 .path
325 .segments
326 .last()
327 .is_some_and(|segment| segment.ident == expected)
328}
329
330fn is_supported_integer(ty: &Type) -> bool {
331 ["i8", "i16", "i32", "i64", "u8", "u16", "u32"]
332 .iter()
333 .any(|expected| type_is(ty, expected))
334}
335
336fn is_supported_float(ty: &Type) -> bool {
337 ["f32", "f64"].iter().any(|expected| type_is(ty, expected))
338}